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
298 lines
11 KiB
TypeScript
298 lines
11 KiB
TypeScript
import type { SapConcurBaseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types'
|
|
import type { OutputProperty } from '@/tools/types'
|
|
|
|
export const scimUserOutputProperties: Record<string, OutputProperty> = {
|
|
id: { type: 'string', description: 'User UUID' },
|
|
externalId: {
|
|
type: 'string',
|
|
description: 'External identifier set by the provisioning client',
|
|
optional: true,
|
|
},
|
|
userName: { type: 'string', description: 'Unique username (often email)' },
|
|
displayName: { type: 'string', description: 'Display name', optional: true },
|
|
nickName: { type: 'string', description: 'Casual or alternate name', optional: true },
|
|
title: { type: 'string', description: 'Job title', optional: true },
|
|
userType: { type: 'string', description: 'User type (e.g., Employee)', optional: true },
|
|
preferredLanguage: { type: 'string', description: 'Preferred language tag', optional: true },
|
|
locale: { type: 'string', description: 'Locale (e.g., en-US)', optional: true },
|
|
timezone: { type: 'string', description: 'Timezone (e.g., America/Los_Angeles)', optional: true },
|
|
active: { type: 'boolean', description: 'Whether the user is active', optional: true },
|
|
dateOfBirth: { type: 'string', description: 'Date of birth (YYYY-MM-DD)', optional: true },
|
|
name: {
|
|
type: 'json',
|
|
description: 'Structured name',
|
|
optional: true,
|
|
properties: {
|
|
formatted: { type: 'string', description: 'Formatted full name', optional: true },
|
|
familyName: { type: 'string', description: 'Family (last) name', optional: true },
|
|
familyNamePrefix: { type: 'string', description: 'Family name prefix', optional: true },
|
|
givenName: { type: 'string', description: 'Given (first) name', optional: true },
|
|
middleName: { type: 'string', description: 'Middle name', optional: true },
|
|
honorificPrefix: { type: 'string', description: 'Honorific prefix', optional: true },
|
|
honorificSuffix: { type: 'string', description: 'Honorific suffix', optional: true },
|
|
},
|
|
},
|
|
emails: {
|
|
type: 'array',
|
|
description: 'Email addresses',
|
|
optional: true,
|
|
items: {
|
|
type: 'json',
|
|
properties: {
|
|
value: { type: 'string', description: 'Email address' },
|
|
type: { type: 'string', description: 'Type (e.g., work, home)', optional: true },
|
|
primary: { type: 'boolean', description: 'Primary email flag', optional: true },
|
|
display: { type: 'string', description: 'Display label', optional: true },
|
|
notifications: {
|
|
type: 'boolean',
|
|
description: 'Whether email notifications are enabled',
|
|
optional: true,
|
|
},
|
|
verified: { type: 'boolean', description: 'Whether the email is verified', optional: true },
|
|
},
|
|
},
|
|
},
|
|
phoneNumbers: {
|
|
type: 'array',
|
|
description: 'Phone numbers',
|
|
optional: true,
|
|
items: {
|
|
type: 'json',
|
|
properties: {
|
|
value: { type: 'string', description: 'Phone number' },
|
|
type: { type: 'string', description: 'Type (work, mobile, fax, etc.)', optional: true },
|
|
primary: { type: 'boolean', description: 'Primary phone flag', optional: true },
|
|
display: { type: 'string', description: 'Display label', optional: true },
|
|
notifications: {
|
|
type: 'boolean',
|
|
description: 'Whether SMS notifications are enabled',
|
|
optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
addresses: {
|
|
type: 'array',
|
|
description: 'Addresses',
|
|
optional: true,
|
|
items: {
|
|
type: 'json',
|
|
properties: {
|
|
type: { type: 'string', description: 'Address type (work, home, etc.)', optional: true },
|
|
formatted: { type: 'string', description: 'Formatted address', optional: true },
|
|
streetAddress: { type: 'string', description: 'Street address', optional: true },
|
|
locality: { type: 'string', description: 'City / locality', optional: true },
|
|
region: { type: 'string', description: 'State / region', optional: true },
|
|
postalCode: { type: 'string', description: 'Postal code', optional: true },
|
|
country: { type: 'string', description: 'ISO 3166-1 country code', optional: true },
|
|
primary: { type: 'boolean', description: 'Primary address flag', optional: true },
|
|
},
|
|
},
|
|
},
|
|
entitlements: {
|
|
type: 'array',
|
|
description: 'Entitlements granted to the user',
|
|
optional: true,
|
|
items: { type: 'json' },
|
|
},
|
|
roles: {
|
|
type: 'array',
|
|
description: 'Roles assigned to the user',
|
|
optional: true,
|
|
items: { type: 'json' },
|
|
},
|
|
schemas: {
|
|
type: 'array',
|
|
description: 'SCIM schemas the resource conforms to',
|
|
optional: true,
|
|
items: { type: 'string' },
|
|
},
|
|
meta: {
|
|
type: 'json',
|
|
description: 'Resource metadata',
|
|
optional: true,
|
|
properties: {
|
|
created: { type: 'string', description: 'Creation timestamp', optional: true },
|
|
lastModified: { type: 'string', description: 'Last modified timestamp', optional: true },
|
|
resourceType: { type: 'string', description: 'Resource type (User)', optional: true },
|
|
location: { type: 'string', description: 'Resource URL', optional: true },
|
|
version: { type: 'string', description: 'ETag version', optional: true },
|
|
},
|
|
},
|
|
emergencyContacts: {
|
|
type: 'array',
|
|
description: 'Emergency contacts',
|
|
optional: true,
|
|
items: {
|
|
type: 'json',
|
|
properties: {
|
|
name: { type: 'string', description: 'Contact full name', optional: true },
|
|
relationship: { type: 'string', description: 'Relationship to user', optional: true },
|
|
emails: { type: 'array', description: 'Emails', optional: true, items: { type: 'json' } },
|
|
phones: { type: 'array', description: 'Phones', optional: true, items: { type: 'json' } },
|
|
streetAddress: { type: 'string', description: 'Street address', optional: true },
|
|
locality: { type: 'string', description: 'City / locality', optional: true },
|
|
region: { type: 'string', description: 'State / region', optional: true },
|
|
postalCode: { type: 'string', description: 'Postal code', optional: true },
|
|
country: { type: 'string', description: 'ISO 3166-1 country code', optional: true },
|
|
},
|
|
},
|
|
},
|
|
localeOverrides: {
|
|
type: 'json',
|
|
description: 'Read-only locale and date/time/number preference overrides',
|
|
optional: true,
|
|
},
|
|
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User': {
|
|
type: 'json',
|
|
description: 'SCIM Enterprise User extension',
|
|
optional: true,
|
|
properties: {
|
|
employeeNumber: { type: 'string', description: 'Employee number', optional: true },
|
|
companyId: { type: 'string', description: 'Concur company identifier', optional: true },
|
|
startDate: { type: 'string', description: 'Employment start date', optional: true },
|
|
terminationDate: {
|
|
type: 'string',
|
|
description: 'Employment termination date',
|
|
optional: true,
|
|
},
|
|
leavesOfAbsence: {
|
|
type: 'array',
|
|
description: 'Leaves of absence',
|
|
optional: true,
|
|
items: {
|
|
type: 'json',
|
|
properties: {
|
|
startDate: { type: 'string', description: 'Leave start date', optional: true },
|
|
endDate: { type: 'string', description: 'Leave end date', optional: true },
|
|
type: { type: 'string', description: 'Leave type', optional: true },
|
|
},
|
|
},
|
|
},
|
|
costCenter: { type: 'string', description: 'Cost center', optional: true },
|
|
organization: { type: 'string', description: 'Organization', optional: true },
|
|
division: { type: 'string', description: 'Division', optional: true },
|
|
department: { type: 'string', description: 'Department', optional: true },
|
|
manager: {
|
|
type: 'json',
|
|
description: 'Manager reference',
|
|
optional: true,
|
|
properties: {
|
|
value: { type: 'string', description: 'Manager UUID', optional: true },
|
|
$ref: { type: 'string', description: 'Manager resource URL', optional: true },
|
|
displayName: { type: 'string', description: 'Manager display name', optional: true },
|
|
employeeNumber: {
|
|
type: 'string',
|
|
description: 'Manager employee number',
|
|
optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
'urn:ietf:params:scim:schemas:extension:sap:2.0:User': {
|
|
type: 'json',
|
|
description: 'SAP SCIM extension',
|
|
optional: true,
|
|
properties: {
|
|
userUuid: { type: 'string', description: 'SAP global user UUID', optional: true },
|
|
},
|
|
},
|
|
'urn:ietf:params:scim:schemas:extension:sap:concur:2.0:User': {
|
|
type: 'json',
|
|
description: 'SAP Concur SCIM extension (Concur-specific attributes)',
|
|
optional: true,
|
|
},
|
|
}
|
|
|
|
export const scimListResponseOutputProperties: Record<string, OutputProperty> = {
|
|
schemas: {
|
|
type: 'array',
|
|
description: 'SCIM schemas the response conforms to',
|
|
optional: true,
|
|
items: { type: 'string' },
|
|
},
|
|
totalResults: {
|
|
type: 'number',
|
|
description: 'Total number of results matching the query',
|
|
optional: true,
|
|
},
|
|
itemsPerPage: {
|
|
type: 'number',
|
|
description: 'Number of results returned in this page',
|
|
optional: true,
|
|
},
|
|
startIndex: {
|
|
type: 'number',
|
|
description: '1-based index of the first result',
|
|
optional: true,
|
|
},
|
|
cursor: {
|
|
type: 'string',
|
|
description: 'SCIM v4.1 cursor for the next page of results',
|
|
optional: true,
|
|
},
|
|
Resources: {
|
|
type: 'array',
|
|
description: 'SCIM User resources',
|
|
optional: true,
|
|
items: {
|
|
type: 'json',
|
|
properties: scimUserOutputProperties,
|
|
},
|
|
},
|
|
}
|
|
|
|
export const SAP_CONCUR_PROXY_URL = '/api/tools/sap_concur/proxy'
|
|
|
|
export function baseProxyBody(params: SapConcurBaseParams): Record<string, unknown> {
|
|
const body: Record<string, unknown> = {
|
|
datacenter: params.datacenter ?? 'us.api.concursolutions.com',
|
|
grantType: params.grantType ?? 'client_credentials',
|
|
clientId: params.clientId,
|
|
clientSecret: params.clientSecret,
|
|
}
|
|
if (params.username) body.username = params.username
|
|
if (params.password) body.password = params.password
|
|
if (params.companyUuid) body.companyUuid = params.companyUuid
|
|
return body
|
|
}
|
|
|
|
export function buildListQuery(
|
|
params: Record<string, string | number | boolean | undefined | null>
|
|
): Record<string, string | number | boolean> {
|
|
const query: Record<string, string | number | boolean> = {}
|
|
for (const [key, value] of Object.entries(params)) {
|
|
if (value === undefined || value === null) continue
|
|
if (typeof value === 'string' && value.trim() === '') continue
|
|
query[key] = value
|
|
}
|
|
return query
|
|
}
|
|
|
|
export async function transformSapConcurProxyResponse(
|
|
response: Response
|
|
): Promise<SapConcurProxyResponse> {
|
|
const data = (await response.json()) as
|
|
| { success: true; output: { status: number; data: unknown } }
|
|
| { success: false; error?: string; status?: number }
|
|
if (!('success' in data) || data.success === false) {
|
|
const errMessage = 'error' in data && data.error ? data.error : 'Concur request failed'
|
|
throw new Error(errMessage)
|
|
}
|
|
return {
|
|
success: true,
|
|
output: {
|
|
status: data.output.status,
|
|
data: data.output.data,
|
|
},
|
|
}
|
|
}
|
|
|
|
export function trimRequired(value: string | undefined, name: string): string {
|
|
if (!value || !value.trim()) {
|
|
throw new Error(`${name} is required`)
|
|
}
|
|
return value.trim()
|
|
}
|