feat: add @type? optional notation
This commit is contained in:
parent
1f3a812728
commit
075045223f
|
|
@ -82,7 +82,11 @@ function resolveReferenceId(
|
|||
}
|
||||
|
||||
function parseReferenceIds(schema: ReferenceSchema, valueString: string): unknown {
|
||||
const valueParser = new ReferenceValueParser(valueString.trim());
|
||||
const trimmed = valueString.trim();
|
||||
if (schema.isOptional && trimmed === '') {
|
||||
return null;
|
||||
}
|
||||
const valueParser = new ReferenceValueParser(trimmed);
|
||||
const ids = valueParser.parseIds(schema.isArray);
|
||||
if (schema.isArray) {
|
||||
return ids;
|
||||
|
|
@ -136,6 +140,7 @@ function parseValueWithReferenceIds(
|
|||
function extractNestedReferenceIds(value: unknown, schema: Schema): unknown {
|
||||
switch (schema.type) {
|
||||
case 'reference':
|
||||
if (value === null || value === undefined) return value;
|
||||
if (schema.isArray) {
|
||||
const ids = Array.isArray(value) ? value : [value];
|
||||
return ids.map(id => String(id));
|
||||
|
|
@ -258,6 +263,7 @@ function resolveNestedReferences(
|
|||
): unknown {
|
||||
switch (schema.type) {
|
||||
case 'reference': {
|
||||
if (value === null || value === undefined) return value;
|
||||
const { lookup } = loadReferenceTable(schema, refBaseDir, defaultPrimaryKey, currentFilePath);
|
||||
if (schema.isArray) {
|
||||
const ids = Array.isArray(value) ? value : [value];
|
||||
|
|
@ -379,9 +385,14 @@ function parseReferenceValue(
|
|||
defaultPrimaryKey: string,
|
||||
currentFilePath: string | undefined
|
||||
): unknown {
|
||||
const trimmed = valueString.trim();
|
||||
if (schema.isOptional && trimmed === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { lookup } = loadReferenceTable(schema, refBaseDir, defaultPrimaryKey, currentFilePath);
|
||||
|
||||
const valueParser = new ReferenceValueParser(valueString.trim());
|
||||
const valueParser = new ReferenceValueParser(trimmed);
|
||||
const ids = valueParser.parseIds(schema.isArray);
|
||||
|
||||
if (schema.isArray) {
|
||||
|
|
@ -497,10 +508,10 @@ function schemaToTypeString(schema: Schema, resourceNames?: Map<string, string>)
|
|||
case 'union':
|
||||
return schema.members.map(m => schemaToTypeString(m, resourceNames)).join(' | ');
|
||||
case 'reference': {
|
||||
// Use the resource name mapping if provided, otherwise capitalize table name
|
||||
const typeName = resourceNames?.get(schema.tableName) ||
|
||||
schema.tableName.charAt(0).toUpperCase() + schema.tableName.slice(1);
|
||||
return schema.isArray ? `readonly ${typeName}[]` : typeName;
|
||||
const baseType = schema.isArray ? `readonly ${typeName}[]` : typeName;
|
||||
return schema.isOptional ? `${baseType} | null` : baseType;
|
||||
}
|
||||
case 'array':
|
||||
if (schema.element.type === 'tuple') {
|
||||
|
|
@ -764,6 +775,12 @@ function generateSchemaResolutionCode(
|
|||
switch (schema.type) {
|
||||
case 'reference': {
|
||||
const lookup = lookupVar(schema.tableName);
|
||||
if (schema.isOptional) {
|
||||
if (schema.isArray) {
|
||||
return `(${valueExpr} === null || ${valueExpr} === undefined ? ${valueExpr} : (Array.isArray(${valueExpr}) ? ${valueExpr}.map(id => ${lookup}.get(String(id))) : ${lookup}.get(String(${valueExpr}))))`;
|
||||
}
|
||||
return `(${valueExpr} === null || ${valueExpr} === undefined ? ${valueExpr} : ${lookup}.get(String(${valueExpr})))`;
|
||||
}
|
||||
if (schema.isArray) {
|
||||
return `(Array.isArray(${valueExpr}) ? ${valueExpr}.map(id => ${lookup}.get(String(id))) : ${valueExpr})`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -302,22 +302,37 @@ describe('parseSchema', () => {
|
|||
describe('Reference schemas (parseSchema)', () => {
|
||||
it('should parse single reference schema @tablename', () => {
|
||||
const schema = parseSchema('@users');
|
||||
expect(schema).toEqual({ type: 'reference', tableName: 'users', isArray: false });
|
||||
expect(schema).toEqual({ type: 'reference', tableName: 'users', isArray: false, isOptional: false });
|
||||
});
|
||||
|
||||
it('should parse array reference schema @tablename[]', () => {
|
||||
const schema = parseSchema('@users[]');
|
||||
expect(schema).toEqual({ type: 'reference', tableName: 'users', isArray: true });
|
||||
expect(schema).toEqual({ type: 'reference', tableName: 'users', isArray: true, isOptional: false });
|
||||
});
|
||||
|
||||
it('should parse reference with hyphens in table name', () => {
|
||||
const schema = parseSchema('@my-table');
|
||||
expect(schema).toEqual({ type: 'reference', tableName: 'my-table', isArray: false });
|
||||
expect(schema).toEqual({ type: 'reference', tableName: 'my-table', isArray: false, isOptional: false });
|
||||
});
|
||||
|
||||
it('should parse reference with underscores in table name', () => {
|
||||
const schema = parseSchema('@my_table');
|
||||
expect(schema).toEqual({ type: 'reference', tableName: 'my_table', isArray: false });
|
||||
expect(schema).toEqual({ type: 'reference', tableName: 'my_table', isArray: false, isOptional: false });
|
||||
});
|
||||
|
||||
it('should parse array reference schema @tablename[]', () => {
|
||||
const schema = parseSchema('@users[]');
|
||||
expect(schema).toEqual({ type: 'reference', tableName: 'users', isArray: true, isOptional: false });
|
||||
});
|
||||
|
||||
it('should parse reference with hyphens in table name', () => {
|
||||
const schema = parseSchema('@my-table');
|
||||
expect(schema).toEqual({ type: 'reference', tableName: 'my-table', isArray: false, isOptional: false });
|
||||
});
|
||||
|
||||
it('should parse reference with underscores in table name', () => {
|
||||
const schema = parseSchema('@my_table');
|
||||
expect(schema).toEqual({ type: 'reference', tableName: 'my_table', isArray: false, isOptional: false });
|
||||
});
|
||||
|
||||
it('should throw ParseError for @ without table name', () => {
|
||||
|
|
@ -334,7 +349,7 @@ describe('Reference schemas (parseSchema)', () => {
|
|||
if (schema.type === 'tuple') {
|
||||
expect(schema.elements).toHaveLength(2);
|
||||
expect(schema.elements[0].schema).toEqual({ type: 'string' });
|
||||
expect(schema.elements[1].schema).toEqual({ type: 'reference', tableName: 'users', isArray: false });
|
||||
expect(schema.elements[1].schema).toEqual({ type: 'reference', tableName: 'users', isArray: false, isOptional: false });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -344,7 +359,7 @@ describe('Reference schemas (parseSchema)', () => {
|
|||
if (schema.type === 'tuple') {
|
||||
expect(schema.elements).toHaveLength(2);
|
||||
expect(schema.elements[0].schema).toEqual({ type: 'string' });
|
||||
expect(schema.elements[1].schema).toEqual({ type: 'reference', tableName: 'users', isArray: true });
|
||||
expect(schema.elements[1].schema).toEqual({ type: 'reference', tableName: 'users', isArray: true, isOptional: false });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -354,7 +369,7 @@ describe('Reference schemas (parseSchema)', () => {
|
|||
if (schema.type === 'tuple') {
|
||||
expect(schema.elements[1]).toEqual({
|
||||
name: 'author',
|
||||
schema: { type: 'reference', tableName: 'users', isArray: false },
|
||||
schema: { type: 'reference', tableName: 'users', isArray: false, isOptional: false },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -365,7 +380,7 @@ describe('Reference schemas (parseSchema)', () => {
|
|||
if (schema.type === 'array') {
|
||||
expect(schema.element.type).toBe('tuple');
|
||||
if (schema.element.type === 'tuple') {
|
||||
expect(schema.element.elements[0].schema).toEqual({ type: 'reference', tableName: 'users', isArray: false });
|
||||
expect(schema.element.elements[0].schema).toEqual({ type: 'reference', tableName: 'users', isArray: false, isOptional: false });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -375,7 +390,7 @@ describe('Reference schemas (parseSchema)', () => {
|
|||
expect(schema.type).toBe('union');
|
||||
if (schema.type === 'union') {
|
||||
expect(schema.members).toHaveLength(2);
|
||||
expect(schema.members[0]).toEqual({ type: 'reference', tableName: 'users', isArray: false });
|
||||
expect(schema.members[0]).toEqual({ type: 'reference', tableName: 'users', isArray: false, isOptional: false });
|
||||
expect(schema.members[1]).toEqual({ type: 'string' });
|
||||
}
|
||||
});
|
||||
|
|
@ -384,7 +399,7 @@ describe('Reference schemas (parseSchema)', () => {
|
|||
const schema = parseSchema('@users[] | string');
|
||||
expect(schema.type).toBe('union');
|
||||
if (schema.type === 'union') {
|
||||
expect(schema.members[0]).toEqual({ type: 'reference', tableName: 'users', isArray: true });
|
||||
expect(schema.members[0]).toEqual({ type: 'reference', tableName: 'users', isArray: true, isOptional: false });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -393,8 +408,8 @@ describe('Reference schemas (parseSchema)', () => {
|
|||
expect(schema.type).toBe('union');
|
||||
if (schema.type === 'union') {
|
||||
expect(schema.members).toHaveLength(2);
|
||||
expect(schema.members[0]).toEqual({ type: 'reference', tableName: 'users', isArray: false });
|
||||
expect(schema.members[1]).toEqual({ type: 'reference', tableName: 'parts', isArray: false });
|
||||
expect(schema.members[0]).toEqual({ type: 'reference', tableName: 'users', isArray: false, isOptional: false });
|
||||
expect(schema.members[1]).toEqual({ type: 'reference', tableName: 'parts', isArray: false, isOptional: false });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -404,11 +419,39 @@ describe('Reference schemas (parseSchema)', () => {
|
|||
if (schema.type === 'array') {
|
||||
expect(schema.element.type).toBe('union');
|
||||
if (schema.element.type === 'union') {
|
||||
expect(schema.element.members[0]).toEqual({ type: 'reference', tableName: 'users', isArray: false });
|
||||
expect(schema.element.members[1]).toEqual({ type: 'reference', tableName: 'parts', isArray: false });
|
||||
expect(schema.element.members[0]).toEqual({ type: 'reference', tableName: 'users', isArray: false, isOptional: false });
|
||||
expect(schema.element.members[1]).toEqual({ type: 'reference', tableName: 'parts', isArray: false, isOptional: false });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should parse optional reference schema @tablename?', () => {
|
||||
const schema = parseSchema('@users?');
|
||||
expect(schema).toEqual({ type: 'reference', tableName: 'users', isArray: false, isOptional: true });
|
||||
});
|
||||
|
||||
it('should parse optional array reference schema @tablename[]?', () => {
|
||||
const schema = parseSchema('@users[]?');
|
||||
expect(schema).toEqual({ type: 'reference', tableName: 'users', isArray: true, isOptional: true });
|
||||
});
|
||||
|
||||
it('should parse optional reference schema @tablename?', () => {
|
||||
const schema = parseSchema('@users?');
|
||||
expect(schema).toEqual({ type: 'reference', tableName: 'users', isArray: false, isOptional: true });
|
||||
});
|
||||
|
||||
it('should parse optional array reference schema @tablename[]?', () => {
|
||||
const schema = parseSchema('@users[]?');
|
||||
expect(schema).toEqual({ type: 'reference', tableName: 'users', isArray: true, isOptional: true });
|
||||
});
|
||||
|
||||
it('should parse optional reference in union @users? | string', () => {
|
||||
const schema = parseSchema('@users? | string');
|
||||
expect(schema.type).toBe('union');
|
||||
if (schema.type === 'union') {
|
||||
expect(schema.members[0]).toEqual({ type: 'reference', tableName: 'users', isArray: false, isOptional: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reference value parsing (parseValue)', () => {
|
||||
|
|
|
|||
|
|
@ -323,18 +323,25 @@ class Parser {
|
|||
|
||||
// Check for array syntax
|
||||
if (this.consumeStr('[]')) {
|
||||
this.skipWhitespace();
|
||||
const isOptional = this.consumeStr('?');
|
||||
return {
|
||||
type: 'reference',
|
||||
tableName,
|
||||
isArray: true,
|
||||
isOptional,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for optional suffix
|
||||
const isOptional = this.consumeStr('?');
|
||||
|
||||
// Single reference (non-array)
|
||||
return {
|
||||
type: 'reference',
|
||||
tableName,
|
||||
isArray: false,
|
||||
isOptional,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ export interface ReferenceSchema {
|
|||
tableName: string;
|
||||
/** Whether it's an array reference */
|
||||
isArray: boolean;
|
||||
/** Whether it's optional (allows empty input resolving to null) */
|
||||
isOptional?: boolean;
|
||||
}
|
||||
|
||||
export interface StringLiteralSchema {
|
||||
|
|
|
|||
|
|
@ -312,7 +312,14 @@ class ValueParser {
|
|||
return result;
|
||||
}
|
||||
|
||||
private parseReferenceValue(schema: ReferenceSchema): string | string[] {
|
||||
private parseReferenceValue(schema: ReferenceSchema): string | string[] | null {
|
||||
if (schema.isOptional) {
|
||||
this.skipWhitespace();
|
||||
if (this.pos >= this.input.length) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.isArray) {
|
||||
// Parse array of IDs: [id1; id2; id3]
|
||||
let hasOpenBracket = false;
|
||||
|
|
@ -413,7 +420,7 @@ export function createValidator(schema: Schema): (value: unknown) => boolean {
|
|||
if (!Array.isArray(value)) return false;
|
||||
return value.every((item) => createValidator(schema.element)(item));
|
||||
case 'reference':
|
||||
// Reference can be a string (single ID) or array of strings (IDs)
|
||||
if (schema.isOptional && value === null) return true;
|
||||
if (schema.isArray) {
|
||||
return Array.isArray(value) && value.every((id) => typeof id === 'string');
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue