Infer record pickers for record-typed logic function workflow inputs (#21494)
## Context Logic functions can declare workflow inputs typed as records or arrays of records (e.g. the People Data Labs enrichment functions), but the workflow builder rendered those as a plain text input with a variable picker, which is not usable. ## What this does - Adds an `objectUniversalIdentifier` link on input schema properties, so a record-typed input is tied to a workspace object. - The SDK build infers it from a `TwentyRecord<'objectUniversalIdentifier'>` marker type in the handler signature, reading the object's universal identifier straight from the source; explicit input schemas can still set the field directly. - The workflow builder renders these inputs as a single record picker or a record multi-select with the variable picker on the right. Selected records are stored as record ids; `TwentyRecord<UID>` is a branded `string`, so the handler signature reflects that it receives ids (a bound variable resolves to whatever the referenced step produced). - The multi-select collapses overflowing chips into a `+N` badge (reusing `ExpandableList`) and its variable picker offers both record objects and fields. - Updates the People Data Labs enrichment inputs as the reference implementation. <img width="802" height="824" alt="CleanShot 2026-06-12 at 16 54 10@2x" src="https://github.com/user-attachments/assets/a0896d74-0aab-49bd-a173-14c578a2e533" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21494?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+154
@@ -0,0 +1,154 @@
|
||||
import { buildToolInputJsonSchema } from '@/logic-function/build-tool-input-json-schema';
|
||||
|
||||
describe('buildToolInputJsonSchema', () => {
|
||||
it('should collapse a record-typed object to an id string with a resolved label', () => {
|
||||
const result = buildToolInputJsonSchema(
|
||||
{
|
||||
type: 'object',
|
||||
objectUniversalIdentifier: 'company-universal-identifier',
|
||||
},
|
||||
(universalIdentifier) =>
|
||||
universalIdentifier === 'company-universal-identifier'
|
||||
? 'Company'
|
||||
: undefined,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'string',
|
||||
description: 'Id of the Company record',
|
||||
});
|
||||
});
|
||||
|
||||
it('should fall back to a generic label when none resolves', () => {
|
||||
expect(
|
||||
buildToolInputJsonSchema({
|
||||
type: 'object',
|
||||
objectUniversalIdentifier: 'company-universal-identifier',
|
||||
}),
|
||||
).toEqual({
|
||||
type: 'string',
|
||||
description: 'Id of the linked record',
|
||||
});
|
||||
});
|
||||
|
||||
it('should collapse arrays of records to arrays of id strings', () => {
|
||||
const result = buildToolInputJsonSchema(
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
objectUniversalIdentifier: 'person-universal-identifier',
|
||||
},
|
||||
},
|
||||
() => 'Person',
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'array',
|
||||
items: { type: 'string', description: 'Id of the Person record' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should collapse a record type to an id string', () => {
|
||||
expect(
|
||||
buildToolInputJsonSchema(
|
||||
{
|
||||
type: 'record',
|
||||
objectUniversalIdentifier: 'company-universal-identifier',
|
||||
},
|
||||
() => 'Company',
|
||||
),
|
||||
).toEqual({ type: 'string', description: 'Id of the Company record' });
|
||||
});
|
||||
|
||||
it('should collapse a records type to an array of id strings', () => {
|
||||
expect(
|
||||
buildToolInputJsonSchema(
|
||||
{
|
||||
type: 'records',
|
||||
objectUniversalIdentifier: 'person-universal-identifier',
|
||||
},
|
||||
() => 'Person',
|
||||
),
|
||||
).toEqual({
|
||||
type: 'array',
|
||||
items: { type: 'string', description: 'Id of the Person record' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should strip custom keywords recursively while keeping standard ones', () => {
|
||||
const result = buildToolInputJsonSchema(
|
||||
{
|
||||
type: 'object',
|
||||
label: 'Params',
|
||||
properties: {
|
||||
company: {
|
||||
type: 'object',
|
||||
objectUniversalIdentifier: 'company-universal-identifier',
|
||||
description: 'A company',
|
||||
},
|
||||
people: {
|
||||
type: 'array',
|
||||
label: 'People',
|
||||
items: {
|
||||
type: 'object',
|
||||
objectUniversalIdentifier: 'person-universal-identifier',
|
||||
},
|
||||
},
|
||||
note: { type: 'string', multiline: true },
|
||||
kind: { type: 'string', enum: ['a', 'b'] },
|
||||
},
|
||||
required: ['company'],
|
||||
},
|
||||
() => 'Company',
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'object',
|
||||
properties: {
|
||||
company: { type: 'string', description: 'Id of the Company record' },
|
||||
people: {
|
||||
type: 'array',
|
||||
items: { type: 'string', description: 'Id of the Company record' },
|
||||
},
|
||||
note: { type: 'string' },
|
||||
kind: { type: 'string', enum: ['a', 'b'] },
|
||||
},
|
||||
required: ['company'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep boolean additionalProperties and sanitize object ones', () => {
|
||||
expect(
|
||||
buildToolInputJsonSchema({
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
}),
|
||||
).toEqual({ type: 'object', additionalProperties: false });
|
||||
|
||||
expect(
|
||||
buildToolInputJsonSchema({
|
||||
type: 'object',
|
||||
additionalProperties: { type: 'string', multiline: true },
|
||||
}),
|
||||
).toEqual({ type: 'object', additionalProperties: { type: 'string' } });
|
||||
});
|
||||
|
||||
it('should leave non-record nodes untouched aside from custom keywords', () => {
|
||||
expect(
|
||||
buildToolInputJsonSchema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
plainObject: { type: 'object' },
|
||||
count: { type: 'number', minimum: 0, maximum: 10 },
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
type: 'object',
|
||||
properties: {
|
||||
plainObject: { type: 'object' },
|
||||
count: { type: 'number', minimum: 0, maximum: 10 },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -86,6 +86,79 @@ describe('getFunctionInputSchema', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should resolve TwentyRecord markers to record schemas', () => {
|
||||
const fileContent = `
|
||||
export const main = (params: {
|
||||
company: TwentyRecord<'company-universal-identifier'>;
|
||||
companies: TwentyRecord<'company-universal-identifier'>[];
|
||||
otherCompanies: Array<TwentyRecord<'company-universal-identifier'>>;
|
||||
}): void => {
|
||||
return;
|
||||
};
|
||||
`;
|
||||
const result = getFunctionInputSchema(fileContent);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
company: {
|
||||
type: 'record',
|
||||
objectUniversalIdentifier: 'company-universal-identifier',
|
||||
},
|
||||
companies: {
|
||||
type: 'records',
|
||||
objectUniversalIdentifier: 'company-universal-identifier',
|
||||
},
|
||||
otherCompanies: {
|
||||
type: 'records',
|
||||
objectUniversalIdentifier: 'company-universal-identifier',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should leave a bare TwentyRecord without a literal argument unresolved', () => {
|
||||
const fileContent = `
|
||||
export const main = (params: { record: TwentyRecord }): void => {
|
||||
return;
|
||||
};
|
||||
`;
|
||||
const result = getFunctionInputSchema(fileContent);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
record: {},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not resolve plain object type references to records', () => {
|
||||
const fileContent = `
|
||||
export const main = (params: {
|
||||
company: Company;
|
||||
companies: Company[];
|
||||
}): void => {
|
||||
return;
|
||||
};
|
||||
`;
|
||||
const result = getFunctionInputSchema(fileContent);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
company: {},
|
||||
companies: { type: 'array', items: {} },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should analyze a complex function correctly', () => {
|
||||
const fileContent = `
|
||||
function testFunction(
|
||||
|
||||
+23
@@ -18,6 +18,29 @@ describe('getInputSchemaFromSourceCode', () => {
|
||||
const result = await getInputSchemaFromSourceCode(fileContent);
|
||||
expect(result).toEqual(DEFAULT_TOOL_INPUT_SCHEMA);
|
||||
});
|
||||
it('should fall back to empty when the params type cannot be inferred', async () => {
|
||||
const fileContent =
|
||||
'function testFunction(params: ImportedAlias) { return }';
|
||||
const result = await getInputSchemaFromSourceCode(fileContent);
|
||||
expect(result).toEqual(DEFAULT_TOOL_INPUT_SCHEMA);
|
||||
});
|
||||
it('should infer record schemas from TwentyRecord markers', async () => {
|
||||
const fileContent = `
|
||||
function testFunction(params: {
|
||||
companies: TwentyRecord<'company-universal-identifier'>[];
|
||||
}) { return }
|
||||
`;
|
||||
const result = await getInputSchemaFromSourceCode(fileContent);
|
||||
expect(result).toEqual({
|
||||
type: 'object',
|
||||
properties: {
|
||||
companies: {
|
||||
type: 'records',
|
||||
objectUniversalIdentifier: 'company-universal-identifier',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should return input from source code', async () => {
|
||||
const fileContent = `
|
||||
function testFunction(
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { isRecordArraySchema } from '@/logic-function/is-record-array-schema';
|
||||
|
||||
describe('isRecordArraySchema', () => {
|
||||
it('returns true for a records schema', () => {
|
||||
expect(
|
||||
isRecordArraySchema({
|
||||
type: 'records',
|
||||
objectUniversalIdentifier: 'person-universal-identifier',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for a records schema without an objectUniversalIdentifier', () => {
|
||||
expect(isRecordArraySchema({ type: 'records' })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for the legacy array of record objects', () => {
|
||||
expect(
|
||||
isRecordArraySchema({
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
objectUniversalIdentifier: 'person-universal-identifier',
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for an array of plain objects', () => {
|
||||
expect(
|
||||
isRecordArraySchema({ type: 'array', items: { type: 'object' } }),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for non-array schemas and nullish input', () => {
|
||||
expect(isRecordArraySchema({ type: 'object' })).toBe(false);
|
||||
expect(isRecordArraySchema(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { isRecordObjectSchema } from '@/logic-function/is-record-object-schema';
|
||||
|
||||
describe('isRecordObjectSchema', () => {
|
||||
it('returns true for a record schema', () => {
|
||||
expect(
|
||||
isRecordObjectSchema({
|
||||
type: 'record',
|
||||
objectUniversalIdentifier: 'company-universal-identifier',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for the legacy object+marker schema', () => {
|
||||
expect(
|
||||
isRecordObjectSchema({
|
||||
type: 'object',
|
||||
objectUniversalIdentifier: 'company-universal-identifier',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for a record schema without an objectUniversalIdentifier', () => {
|
||||
expect(isRecordObjectSchema({ type: 'record' })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for a plain object schema', () => {
|
||||
expect(isRecordObjectSchema({ type: 'object' })).toBe(false);
|
||||
expect(
|
||||
isRecordObjectSchema({ type: 'object', objectUniversalIdentifier: '' }),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for non-object schemas and nullish input', () => {
|
||||
expect(
|
||||
isRecordObjectSchema({
|
||||
type: 'string',
|
||||
objectUniversalIdentifier: 'company-universal-identifier',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(isRecordObjectSchema(undefined)).toBe(false);
|
||||
expect(isRecordObjectSchema(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
+56
@@ -140,6 +140,62 @@ describe('jsonSchemaToInputSchema', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves objectUniversalIdentifier on object properties and array items', () => {
|
||||
const result = jsonSchemaToInputSchema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
company: {
|
||||
type: 'object',
|
||||
objectUniversalIdentifier: 'company-universal-identifier',
|
||||
},
|
||||
people: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
objectUniversalIdentifier: 'person-universal-identifier',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result[0].properties?.company).toEqual({
|
||||
type: 'object',
|
||||
objectUniversalIdentifier: 'company-universal-identifier',
|
||||
});
|
||||
expect(result[0].properties?.people).toEqual({
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
objectUniversalIdentifier: 'person-universal-identifier',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('passes record/records types through with objectUniversalIdentifier', () => {
|
||||
const result = jsonSchemaToInputSchema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
company: {
|
||||
type: 'record',
|
||||
objectUniversalIdentifier: 'company-universal-identifier',
|
||||
},
|
||||
people: {
|
||||
type: 'records',
|
||||
objectUniversalIdentifier: 'person-universal-identifier',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result[0].properties?.company).toEqual({
|
||||
type: 'record',
|
||||
objectUniversalIdentifier: 'company-universal-identifier',
|
||||
});
|
||||
expect(result[0].properties?.people).toEqual({
|
||||
type: 'records',
|
||||
objectUniversalIdentifier: 'person-universal-identifier',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not set label when empty or omitted', () => {
|
||||
const result = jsonSchemaToInputSchema({
|
||||
type: 'object',
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { isNonEmptyString, isObject } from '@sniptt/guards';
|
||||
|
||||
import { type InputJsonSchema } from '@/logic-function/input-json-schema.type';
|
||||
import { isRecordObjectSchema } from '@/logic-function/is-record-object-schema';
|
||||
import { isDefined } from '@/utils/validation/isDefined';
|
||||
|
||||
type ResolveObjectLabel = (
|
||||
objectUniversalIdentifier: string,
|
||||
) => string | undefined;
|
||||
|
||||
const buildRecordIdDescription = (
|
||||
objectUniversalIdentifier: string | undefined,
|
||||
resolveObjectLabel?: ResolveObjectLabel,
|
||||
): string => {
|
||||
const objectLabel = isNonEmptyString(objectUniversalIdentifier)
|
||||
? resolveObjectLabel?.(objectUniversalIdentifier)
|
||||
: undefined;
|
||||
|
||||
return `Id of the ${isNonEmptyString(objectLabel) ? objectLabel : 'linked'} record`;
|
||||
};
|
||||
|
||||
export const buildToolInputJsonSchema = (
|
||||
jsonSchema: InputJsonSchema,
|
||||
resolveObjectLabel?: ResolveObjectLabel,
|
||||
): InputJsonSchema => {
|
||||
if (isRecordObjectSchema(jsonSchema)) {
|
||||
return {
|
||||
type: 'string',
|
||||
description: buildRecordIdDescription(
|
||||
jsonSchema.objectUniversalIdentifier,
|
||||
resolveObjectLabel,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (jsonSchema.type === 'records') {
|
||||
return {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
description: buildRecordIdDescription(
|
||||
jsonSchema.objectUniversalIdentifier,
|
||||
resolveObjectLabel,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const {
|
||||
objectUniversalIdentifier: _objectUniversalIdentifier,
|
||||
multiline: _multiline,
|
||||
label: _label,
|
||||
items,
|
||||
properties,
|
||||
additionalProperties,
|
||||
...standardKeywords
|
||||
} = jsonSchema;
|
||||
|
||||
const toolSchema: InputJsonSchema = { ...standardKeywords };
|
||||
|
||||
if (isDefined(items)) {
|
||||
toolSchema.items = buildToolInputJsonSchema(items, resolveObjectLabel);
|
||||
}
|
||||
|
||||
if (isDefined(properties)) {
|
||||
toolSchema.properties = Object.fromEntries(
|
||||
Object.entries(properties).map(([key, value]) => [
|
||||
key,
|
||||
buildToolInputJsonSchema(value, resolveObjectLabel),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(additionalProperties)) {
|
||||
toolSchema.additionalProperties = isObject(additionalProperties)
|
||||
? buildToolInputJsonSchema(additionalProperties, resolveObjectLabel)
|
||||
: additionalProperties;
|
||||
}
|
||||
|
||||
return toolSchema;
|
||||
};
|
||||
@@ -20,6 +20,32 @@ import {
|
||||
import { type InputJsonSchema } from '@/logic-function';
|
||||
import { isDefined } from '@/utils/validation/isDefined';
|
||||
|
||||
const TWENTY_RECORD_TYPE_NAME = 'TwentyRecord';
|
||||
|
||||
const getObjectUniversalIdentifierFromTypeArgument = (
|
||||
typeReferenceNode: TypeReferenceNode,
|
||||
): string | undefined => {
|
||||
const typeArgument = typeReferenceNode.typeArguments?.[0];
|
||||
|
||||
if (
|
||||
isDefined(typeArgument) &&
|
||||
typeArgument.kind === SyntaxKind.LiteralType &&
|
||||
(typeArgument as LiteralTypeNode).literal.kind === SyntaxKind.StringLiteral
|
||||
) {
|
||||
return ((typeArgument as LiteralTypeNode).literal as StringLiteral).text;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const buildArraySchemaFromItems = (items: InputJsonSchema): InputJsonSchema =>
|
||||
items.type === 'record'
|
||||
? {
|
||||
type: 'records',
|
||||
objectUniversalIdentifier: items.objectUniversalIdentifier,
|
||||
}
|
||||
: { type: 'array', items };
|
||||
|
||||
const getTypeString = (typeNode: TypeNode): InputJsonSchema => {
|
||||
switch (typeNode.kind) {
|
||||
case SyntaxKind.NumberKeyword:
|
||||
@@ -29,10 +55,9 @@ const getTypeString = (typeNode: TypeNode): InputJsonSchema => {
|
||||
case SyntaxKind.BooleanKeyword:
|
||||
return { type: 'boolean' };
|
||||
case SyntaxKind.ArrayType:
|
||||
return {
|
||||
type: 'array',
|
||||
items: getTypeString((typeNode as ArrayTypeNode).elementType),
|
||||
};
|
||||
return buildArraySchemaFromItems(
|
||||
getTypeString((typeNode as ArrayTypeNode).elementType),
|
||||
);
|
||||
case SyntaxKind.TypeReference: {
|
||||
const typeReferenceNode = typeNode as TypeReferenceNode;
|
||||
const typeName =
|
||||
@@ -43,10 +68,18 @@ const getTypeString = (typeNode: TypeNode): InputJsonSchema => {
|
||||
if (typeName === 'Array' || typeName === 'ReadonlyArray') {
|
||||
const elementType = typeReferenceNode.typeArguments?.[0];
|
||||
|
||||
return {
|
||||
type: 'array',
|
||||
items: isDefined(elementType) ? getTypeString(elementType) : {},
|
||||
};
|
||||
return buildArraySchemaFromItems(
|
||||
isDefined(elementType) ? getTypeString(elementType) : {},
|
||||
);
|
||||
}
|
||||
|
||||
if (typeName === TWENTY_RECORD_TYPE_NAME) {
|
||||
const objectUniversalIdentifier =
|
||||
getObjectUniversalIdentifierFromTypeArgument(typeReferenceNode);
|
||||
|
||||
if (isDefined(objectUniversalIdentifier)) {
|
||||
return { type: 'record', objectUniversalIdentifier };
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
|
||||
@@ -11,7 +11,6 @@ export const getInputSchemaFromSourceCode = async (
|
||||
await import('./get-function-input-schema');
|
||||
const inputSchema = getFunctionInputSchema(sourceCode);
|
||||
|
||||
// Logic functions take a single params object
|
||||
const firstParam = inputSchema[0];
|
||||
|
||||
if (firstParam?.type === 'object' && isDefined(firstParam.properties)) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* |___/
|
||||
*/
|
||||
|
||||
export { buildToolInputJsonSchema } from './build-tool-input-json-schema';
|
||||
export { DEFAULT_TOOL_INPUT_SCHEMA } from './constants/DefaultToolInputSchema';
|
||||
export { SEED_WORKFLOW_ACTION_TRIGGER_SETTINGS } from './constants/SeedWorkflowActionTriggerSettings';
|
||||
export { getInputSchemaFromSourceCode } from './get-input-schema-from-source-code';
|
||||
@@ -14,4 +15,6 @@ export { getOutputSchemaFromValue } from './get-output-schema-from-value';
|
||||
export { getOutputSchemaMismatchIssues } from './get-output-schema-mismatch-issues';
|
||||
export type { InputJsonSchema } from './input-json-schema.type';
|
||||
export { inputSchemaToOutputSchema } from './input-schema-to-output-schema';
|
||||
export { isRecordArraySchema } from './is-record-array-schema';
|
||||
export { isRecordObjectSchema } from './is-record-object-schema';
|
||||
export { jsonSchemaToInputSchema } from './json-schema-to-input-schema';
|
||||
|
||||
@@ -6,7 +6,9 @@ export type InputJsonSchema = {
|
||||
| 'object'
|
||||
| 'array'
|
||||
| 'integer'
|
||||
| 'null';
|
||||
| 'null'
|
||||
| 'record'
|
||||
| 'records';
|
||||
description?: string;
|
||||
enum?: unknown[];
|
||||
items?: InputJsonSchema;
|
||||
@@ -17,4 +19,5 @@ export type InputJsonSchema = {
|
||||
maximum?: number;
|
||||
multiline?: boolean;
|
||||
label?: string;
|
||||
objectUniversalIdentifier?: string;
|
||||
};
|
||||
|
||||
@@ -28,6 +28,14 @@ const convertProperty = (
|
||||
): Leaf | Node => {
|
||||
const label = property.label ?? key;
|
||||
|
||||
if (property.type === 'record') {
|
||||
return { isLeaf: true, type: 'string', label, value: null };
|
||||
}
|
||||
|
||||
if (property.type === 'records') {
|
||||
return { isLeaf: true, type: 'array', label, value: null };
|
||||
}
|
||||
|
||||
if (property.type === 'object') {
|
||||
return {
|
||||
isLeaf: false,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { isRecordObjectSchema } from '@/logic-function/is-record-object-schema';
|
||||
|
||||
type RecordArraySchema = {
|
||||
type?: string;
|
||||
objectUniversalIdentifier?: string;
|
||||
items?: {
|
||||
type?: string;
|
||||
objectUniversalIdentifier?: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export const isRecordArraySchema = (
|
||||
schema: RecordArraySchema | null | undefined,
|
||||
): boolean =>
|
||||
(schema?.type === 'records' &&
|
||||
isNonEmptyString(schema.objectUniversalIdentifier)) ||
|
||||
(schema?.type === 'array' && isRecordObjectSchema(schema?.items));
|
||||
@@ -0,0 +1,12 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
type RecordObjectSchema = {
|
||||
type?: string;
|
||||
objectUniversalIdentifier?: string;
|
||||
};
|
||||
|
||||
export const isRecordObjectSchema = <TSchema extends RecordObjectSchema>(
|
||||
schema: TSchema | null | undefined,
|
||||
): schema is TSchema & { objectUniversalIdentifier: string } =>
|
||||
(schema?.type === 'record' || schema?.type === 'object') &&
|
||||
isNonEmptyString(schema.objectUniversalIdentifier);
|
||||
@@ -36,6 +36,12 @@ const convertProperty = (jsonSchema: InputJsonSchema): InputSchemaProperty => {
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'record':
|
||||
property.type = 'record';
|
||||
break;
|
||||
case 'records':
|
||||
property.type = 'records';
|
||||
break;
|
||||
case 'null':
|
||||
default:
|
||||
property.type = 'unknown';
|
||||
@@ -55,6 +61,10 @@ const convertProperty = (jsonSchema: InputJsonSchema): InputSchemaProperty => {
|
||||
property.label = jsonSchema.label;
|
||||
}
|
||||
|
||||
if (isNonEmptyString(jsonSchema.objectUniversalIdentifier)) {
|
||||
property.objectUniversalIdentifier = jsonSchema.objectUniversalIdentifier;
|
||||
}
|
||||
|
||||
return property;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user