d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
181 lines
5.0 KiB
TypeScript
181 lines
5.0 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import type { CanvasLayout } from '@/tools/sharepoint/types'
|
|
|
|
const logger = createLogger('SharepointUtils')
|
|
|
|
export function optionalTrim(value: unknown): string | undefined {
|
|
if (value === undefined || value === null) return undefined
|
|
const trimmed = String(value).trim()
|
|
return trimmed || undefined
|
|
}
|
|
|
|
export function escapeODataString(value: string): string {
|
|
return value.replace(/'/g, "''")
|
|
}
|
|
|
|
export function escapeHtml(value: string): string {
|
|
return value
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''')
|
|
}
|
|
|
|
export function getGraphNextPageUrl(data: object): string | undefined {
|
|
const nextLink = (data as Record<string, unknown>)['@odata.nextLink']
|
|
return typeof nextLink === 'string' ? nextLink : undefined
|
|
}
|
|
|
|
export function assertGraphNextPageUrl(nextPageUrl: string): string {
|
|
const trimmed = nextPageUrl.trim()
|
|
const url = new URL(trimmed)
|
|
if (url.origin !== 'https://graph.microsoft.com') {
|
|
throw new Error('nextPageUrl must be a Microsoft Graph @odata.nextLink URL')
|
|
}
|
|
return url.toString()
|
|
}
|
|
|
|
function stripHtmlTags(html: string): string {
|
|
let text = html
|
|
let previous: string
|
|
|
|
do {
|
|
previous = text
|
|
text = text.replace(/<[^>]*>/g, '')
|
|
text = text.replace(/[<>]/g, '')
|
|
} while (text !== previous)
|
|
|
|
return text.trim()
|
|
}
|
|
|
|
export function extractTextFromCanvasLayout(canvasLayout: CanvasLayout | null | undefined): string {
|
|
logger.info('Extracting text from canvas layout', {
|
|
hasCanvasLayout: !!canvasLayout,
|
|
hasHorizontalSections: !!canvasLayout?.horizontalSections,
|
|
sectionsCount: canvasLayout?.horizontalSections?.length || 0,
|
|
})
|
|
|
|
if (!canvasLayout?.horizontalSections) {
|
|
logger.info('No canvas layout or horizontal sections found')
|
|
return ''
|
|
}
|
|
|
|
const textParts: string[] = []
|
|
|
|
for (const section of canvasLayout.horizontalSections) {
|
|
logger.info('Processing section', {
|
|
sectionId: section.id,
|
|
hasColumns: !!section.columns,
|
|
hasWebparts: !!section.webparts,
|
|
columnsCount: section.columns?.length || 0,
|
|
})
|
|
|
|
if (section.columns) {
|
|
for (const column of section.columns) {
|
|
if (column.webparts) {
|
|
for (const webpart of column.webparts) {
|
|
logger.info('Processing webpart', {
|
|
webpartId: webpart.id,
|
|
hasInnerHtml: !!webpart.innerHtml,
|
|
innerHtml: webpart.innerHtml,
|
|
})
|
|
|
|
if (webpart.innerHtml) {
|
|
const text = stripHtmlTags(webpart.innerHtml)
|
|
if (text) {
|
|
textParts.push(text)
|
|
logger.info('Extracted text', { text })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (section.webparts) {
|
|
for (const webpart of section.webparts) {
|
|
if (webpart.innerHtml) {
|
|
const text = stripHtmlTags(webpart.innerHtml)
|
|
if (text) textParts.push(text)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const finalContent = textParts.join('\n\n')
|
|
logger.info('Final extracted content', {
|
|
textPartsCount: textParts.length,
|
|
finalContentLength: finalContent.length,
|
|
finalContent,
|
|
})
|
|
|
|
return finalContent
|
|
}
|
|
|
|
/** SharePoint list item fields that are system-managed and cannot be set via the Graph API. */
|
|
export const READ_ONLY_LIST_ITEM_FIELDS = new Set<string>([
|
|
'Id',
|
|
'id',
|
|
'UniqueId',
|
|
'GUID',
|
|
'ContentTypeId',
|
|
'Created',
|
|
'Modified',
|
|
'Author',
|
|
'Editor',
|
|
'CreatedBy',
|
|
'ModifiedBy',
|
|
'AuthorId',
|
|
'EditorId',
|
|
'_UIVersionString',
|
|
'Attachments',
|
|
'FileRef',
|
|
'FileDirRef',
|
|
'FileLeafRef',
|
|
])
|
|
|
|
/**
|
|
* Removes read-only/system-managed fields from a SharePoint list item field set, logging any
|
|
* fields that were stripped. Throws if no updatable fields remain.
|
|
*/
|
|
export function sanitizeListItemFields(
|
|
fields: Record<string, unknown>,
|
|
context: { action: 'update' | 'create' }
|
|
): Record<string, unknown> {
|
|
const entries = Object.entries(fields)
|
|
const updatableEntries = entries.filter(([key]) => !READ_ONLY_LIST_ITEM_FIELDS.has(key))
|
|
|
|
if (updatableEntries.length !== entries.length) {
|
|
const removed = entries
|
|
.filter(([key]) => READ_ONLY_LIST_ITEM_FIELDS.has(key))
|
|
.map(([key]) => key)
|
|
logger.warn(`Removed read-only SharePoint fields from ${context.action}`, { removed })
|
|
}
|
|
|
|
if (updatableEntries.length === 0) {
|
|
const requestedKeys = Object.keys(fields)
|
|
const verb = context.action === 'update' ? 'updated' : 'set'
|
|
throw new Error(
|
|
`All provided fields are read-only and cannot be ${verb}: ${requestedKeys.join(', ')}`
|
|
)
|
|
}
|
|
|
|
return Object.fromEntries(updatableEntries)
|
|
}
|
|
|
|
export function cleanODataMetadata<T>(obj: T): T {
|
|
if (!obj || typeof obj !== 'object') return obj
|
|
|
|
if (Array.isArray(obj)) {
|
|
return obj.map((item) => cleanODataMetadata(item)) as T
|
|
}
|
|
|
|
const cleaned: Record<string, unknown> = {}
|
|
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
if (key.includes('@odata')) continue
|
|
|
|
cleaned[key] = cleanODataMetadata(value)
|
|
}
|
|
|
|
return cleaned as T
|
|
}
|