Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
211 lines
6.7 KiB
TypeScript
211 lines
6.7 KiB
TypeScript
import type { ExpressionMode } from './modes';
|
|
import type {
|
|
SharedExpressionContextRoot,
|
|
SharedExpressionPathOption,
|
|
SharedExpressionSchemaNode,
|
|
SharedExpressionValueType,
|
|
} from './context';
|
|
|
|
const ARRAY_SEGMENT = '[]';
|
|
|
|
const normalizeSchemaType = (schema: SharedExpressionSchemaNode | undefined): SharedExpressionValueType => {
|
|
if (!schema?.type) return 'unknown';
|
|
const rawType = Array.isArray(schema.type)
|
|
? schema.type.find((entry) => entry !== 'null') ?? schema.type[0]
|
|
: schema.type;
|
|
if (
|
|
rawType === 'string' ||
|
|
rawType === 'number' ||
|
|
rawType === 'integer' ||
|
|
rawType === 'boolean' ||
|
|
rawType === 'object' ||
|
|
rawType === 'array' ||
|
|
rawType === 'null'
|
|
) {
|
|
return rawType;
|
|
}
|
|
return 'unknown';
|
|
};
|
|
|
|
const decodeJsonPointerSegment = (segment: string): string =>
|
|
segment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
|
|
const resolveLocalSchemaRef = (
|
|
ref: string | undefined,
|
|
rootSchema: SharedExpressionSchemaNode | undefined
|
|
): SharedExpressionSchemaNode | undefined => {
|
|
if (!ref || !rootSchema) return undefined;
|
|
if (ref === '#') return rootSchema;
|
|
if (!ref.startsWith('#/')) return undefined;
|
|
|
|
const parts = ref.slice(2).split('/').map(decodeJsonPointerSegment);
|
|
let current: unknown = rootSchema;
|
|
for (const part of parts) {
|
|
if (!current || typeof current !== 'object') return undefined;
|
|
current = (current as Record<string, unknown>)[part];
|
|
}
|
|
|
|
return current && typeof current === 'object'
|
|
? (current as SharedExpressionSchemaNode)
|
|
: undefined;
|
|
};
|
|
|
|
const resolveSchema = (
|
|
schema: SharedExpressionSchemaNode | undefined,
|
|
rootSchema?: SharedExpressionSchemaNode,
|
|
seenRefs = new Set<string>()
|
|
): SharedExpressionSchemaNode | undefined => {
|
|
if (!schema) return undefined;
|
|
const root = rootSchema ?? schema;
|
|
|
|
if (schema.$ref && !seenRefs.has(schema.$ref)) {
|
|
seenRefs.add(schema.$ref);
|
|
const referenced = resolveLocalSchemaRef(schema.$ref, root);
|
|
if (referenced) {
|
|
return resolveSchema(referenced, root, seenRefs);
|
|
}
|
|
}
|
|
|
|
if (schema.anyOf?.length) {
|
|
const nonNull = schema.anyOf.find((variant) => normalizeSchemaType(variant) !== 'null');
|
|
if (nonNull) {
|
|
return resolveSchema(nonNull, root, seenRefs);
|
|
}
|
|
}
|
|
|
|
if (schema.oneOf?.length) {
|
|
return resolveSchema(schema.oneOf[0], root, seenRefs);
|
|
}
|
|
|
|
if (schema.allOf?.length) {
|
|
return resolveSchema(schema.allOf[0], root, seenRefs);
|
|
}
|
|
|
|
return schema;
|
|
};
|
|
|
|
const joinPath = (root: string, segments: string[]): string =>
|
|
segments.reduce((acc, segment) => (segment === ARRAY_SEGMENT ? `${acc}${ARRAY_SEGMENT}` : `${acc}.${segment}`), root);
|
|
|
|
const hasChildNodes = (schema: SharedExpressionSchemaNode | undefined): boolean => {
|
|
if (!schema) return false;
|
|
const normalized = resolveSchema(schema, schema);
|
|
if (!normalized) return false;
|
|
if (normalized.properties && Object.keys(normalized.properties).length > 0) return true;
|
|
if (normalized.items) return true;
|
|
if (normalized.additionalProperties && typeof normalized.additionalProperties === 'object') return true;
|
|
return false;
|
|
};
|
|
|
|
const createOption = (
|
|
root: SharedExpressionContextRoot,
|
|
segments: string[],
|
|
schema: SharedExpressionSchemaNode | undefined,
|
|
rootSchema: SharedExpressionSchemaNode | undefined = root.schema
|
|
): SharedExpressionPathOption => {
|
|
const path = joinPath(root.key, segments);
|
|
const resolvedSchema = resolveSchema(schema, rootSchema);
|
|
|
|
return {
|
|
root: root.key,
|
|
path,
|
|
label: path,
|
|
description: resolvedSchema?.description,
|
|
valueType: normalizeSchemaType(resolvedSchema),
|
|
depth: segments.length,
|
|
isLeaf: !hasChildNodes(resolvedSchema),
|
|
segments: [root.key, ...segments],
|
|
};
|
|
};
|
|
|
|
const pushObjectPropertyOptions = (
|
|
options: SharedExpressionPathOption[],
|
|
root: SharedExpressionContextRoot,
|
|
parentSegments: string[],
|
|
schema: SharedExpressionSchemaNode,
|
|
rootSchema: SharedExpressionSchemaNode
|
|
) => {
|
|
const propertyEntries = Object.entries(schema.properties ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
for (const [propertyName, propertySchema] of propertyEntries) {
|
|
const nextSegments = [...parentSegments, propertyName];
|
|
const nextRootSchema =
|
|
root.key === 'vars' && parentSegments.length === 0 && propertySchema && typeof propertySchema === 'object'
|
|
? propertySchema
|
|
: rootSchema;
|
|
options.push(createOption(root, nextSegments, propertySchema, nextRootSchema));
|
|
pushNestedOptions(options, root, nextSegments, propertySchema, nextRootSchema);
|
|
}
|
|
};
|
|
|
|
const pushArrayItemOptions = (
|
|
options: SharedExpressionPathOption[],
|
|
root: SharedExpressionContextRoot,
|
|
parentSegments: string[],
|
|
schema: SharedExpressionSchemaNode,
|
|
rootSchema: SharedExpressionSchemaNode
|
|
) => {
|
|
const resolvedArray = resolveSchema(schema, rootSchema);
|
|
const itemSchema = resolvedArray?.items ? resolveSchema(resolvedArray.items, rootSchema) : undefined;
|
|
if (!itemSchema) return;
|
|
|
|
const itemSegments = [...parentSegments, ARRAY_SEGMENT];
|
|
options.push(createOption(root, itemSegments, itemSchema));
|
|
pushNestedOptions(options, root, itemSegments, itemSchema, rootSchema);
|
|
};
|
|
|
|
const pushNestedOptions = (
|
|
options: SharedExpressionPathOption[],
|
|
root: SharedExpressionContextRoot,
|
|
currentSegments: string[],
|
|
schema: SharedExpressionSchemaNode,
|
|
rootSchema: SharedExpressionSchemaNode
|
|
) => {
|
|
const resolved = resolveSchema(schema, rootSchema);
|
|
if (!resolved) return;
|
|
|
|
const type = normalizeSchemaType(resolved);
|
|
if (type === 'object') {
|
|
pushObjectPropertyOptions(options, root, currentSegments, resolved, rootSchema);
|
|
if (resolved.additionalProperties && typeof resolved.additionalProperties === 'object') {
|
|
const mapSegments = [...currentSegments, '*'];
|
|
options.push(createOption(root, mapSegments, resolved.additionalProperties));
|
|
pushNestedOptions(options, root, mapSegments, resolved.additionalProperties, rootSchema);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (type === 'array') {
|
|
pushArrayItemOptions(options, root, currentSegments, resolved, rootSchema);
|
|
}
|
|
};
|
|
|
|
export type BuildPathOptionsParams = {
|
|
mode?: ExpressionMode;
|
|
includeRootPaths?: boolean;
|
|
};
|
|
|
|
export const buildPathOptionsFromContextRoots = (
|
|
roots: SharedExpressionContextRoot[],
|
|
params: BuildPathOptionsParams = {}
|
|
): SharedExpressionPathOption[] => {
|
|
const includeRootPaths = params.includeRootPaths ?? true;
|
|
|
|
const options: SharedExpressionPathOption[] = [];
|
|
const sortedRoots = [...roots].sort((a, b) => a.key.localeCompare(b.key));
|
|
|
|
for (const root of sortedRoots) {
|
|
if (params.mode && root.allowInModes && !root.allowInModes.includes(params.mode)) {
|
|
continue;
|
|
}
|
|
|
|
if (includeRootPaths) {
|
|
options.push(createOption(root, [], root.schema));
|
|
}
|
|
|
|
if (!root.schema) continue;
|
|
pushNestedOptions(options, root, [], root.schema, root.schema);
|
|
}
|
|
|
|
return options;
|
|
};
|