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