diff --git a/src/genSchema/ensureRecordSchema.test.ts b/src/genSchema/ensureRecordSchema.test.ts index baf7ec6..404c0f1 100644 --- a/src/genSchema/ensureRecordSchema.test.ts +++ b/src/genSchema/ensureRecordSchema.test.ts @@ -6,8 +6,6 @@ const RecordIdValue = z.union([z.string(), z.number(), z.bigint(), z.record(z.un type RecordIdValue = z.infer -type TableRecordId = RecordId | StringRecordId | `${T}:${string}` - function recordId(table?: Table) { const tableRegex = table ? table : '[A-Za-z_][A-Za-z0-9_]*' const idRegex = '[^:]+' @@ -30,40 +28,93 @@ function recordId
(table?: Table) { ? `Invalid record ID format. Must be '${table}:id'` : "Invalid record ID format. Must be 'table:id'", }), + z.object({ + rid: z.string().regex(fullRegex), + }), + z + .object({ + tb: z.string(), + id: z.union([z.string(), z.number(), z.record(z.unknown())]), + }) + .refine(val => !table || val.tb === table, { + message: table ? `RecordId must be of type '${table}'` : undefined, + }), ]) - .transform((val): TableRecordId
=> { + .transform((val): RecordId
| StringRecordId => { + if (val instanceof RecordId) { + return val as RecordId
+ } + if (val instanceof StringRecordId) { + return val + } if (typeof val === 'string') { - return new StringRecordId(val) as TableRecordId
+ const [tb, ...idParts] = val.split(':') + const id = idParts.join(':') + if (!tb || !id) throw new Error('Invalid record ID string format') + return new StringRecordId(val) } - return val as TableRecordId
+ if ('rid' in val) { + const [tb, ...idParts] = val.rid.split(':') + const id = idParts.join(':') + if (!tb || !id) throw new Error('Invalid rid object format') + return new StringRecordId(val.rid) + } + if ('tb' in val && 'id' in val) { + return new RecordId(val.tb, val.id) as RecordId
+ } + throw new Error('Invalid input for RecordId') }) } describe('recordId type tests', () => { const createRecordId = (tb: string, id: RecordIdValue) => new RecordId(tb, id) - // biome-ignore lint/suspicious/noExplicitAny: - const createStringRecordId = (tb: string, id: string | number | object | any[]) => - new StringRecordId(`${tb}:${typeof id === 'object' ? JSON.stringify(id) : id}`) + const createStringRecordId = (tb: string, id: RecordIdValue) => { + const idStr = typeof id === 'object' ? JSON.stringify(id) : String(id) + return new StringRecordId(`${tb}:${idStr}`) + } test('Valid simple RecordId', () => { const schema = recordId() const result = schema.safeParse(createRecordId('internet', 'test')) expect(result.success).toBe(true) + if (result.success) { + expect(result.data instanceof RecordId || result.data instanceof StringRecordId).toBe(true) + if (result.data instanceof RecordId) { + expect(result.data.tb).toBe('internet') + expect(result.data.id).toBe('test') + } else { + expect(result.data.rid).toBe('internet:test') + } + } }) test('Valid simple StringRecordId', () => { const schema = recordId() const result = schema.safeParse(createStringRecordId('internet', 'test')) expect(result.success).toBe(true) + if (result.success) { + expect(result.data instanceof RecordId || result.data instanceof StringRecordId).toBe(true) + if (result.data instanceof RecordId) { + expect(result.data.tb).toBe('internet') + expect(result.data.id).toBe('test') + } else { + expect(result.data.rid).toBe('internet:test') + } + } }) - test('Valid simple string (transformed to StringRecordId)', () => { + test('Valid simple string', () => { const schema = recordId() const result = schema.safeParse('internet:test') expect(result.success).toBe(true) if (result.success) { - expect(result.data).toBeInstanceOf(StringRecordId) - expect((result.data as StringRecordId).rid).toBe('internet:test') + expect(result.data instanceof RecordId || result.data instanceof StringRecordId).toBe(true) + if (result.data instanceof RecordId) { + expect(result.data.tb).toBe('internet') + expect(result.data.id).toBe('test') + } else { + expect(result.data.rid).toBe('internet:test') + } } }) @@ -77,56 +128,100 @@ describe('recordId type tests', () => { const schema = recordId() const result = schema.safeParse(createRecordId('internet', 9000)) expect(result.success).toBe(true) + if (result.success) { + expect(result.data instanceof RecordId || result.data instanceof StringRecordId).toBe(true) + if (result.data instanceof RecordId) { + expect(result.data.tb).toBe('internet') + expect(result.data.id).toBe(9000) + } else { + expect(result.data.rid).toBe('internet:9000') + } + } }) test('Valid numeric StringRecordId', () => { const schema = recordId() const result = schema.safeParse(createStringRecordId('internet', 9000)) expect(result.success).toBe(true) + if (result.success) { + expect(result.data instanceof RecordId || result.data instanceof StringRecordId).toBe(true) + if (result.data instanceof RecordId) { + expect(result.data.tb).toBe('internet') + expect(result.data.id).toBe('9000') + } else { + expect(result.data.rid).toBe('internet:9000') + } + } }) test('Valid object-based RecordId', () => { const schema = recordId() - const result = schema.safeParse(createRecordId('temperature', { location: 'London', date: new Date() })) + const objId = { location: 'London', date: new Date().toISOString() } + const result = schema.safeParse(createRecordId('temperature', objId)) expect(result.success).toBe(true) + if (result.success) { + expect(result.data instanceof RecordId || result.data instanceof StringRecordId).toBe(true) + if (result.data instanceof RecordId) { + expect(result.data.tb).toBe('temperature') + expect(result.data.id).toEqual(objId) + } else { + expect(result.data.rid).toBe(`temperature:${JSON.stringify(objId)}`) + } + } }) test('Valid object-based StringRecordId', () => { const schema = recordId() - const result = schema.safeParse( - createStringRecordId('temperature', { location: 'London', date: new Date().toISOString() }), - ) + const objId = { location: 'London', date: new Date().toISOString() } + const result = schema.safeParse(createStringRecordId('temperature', objId)) expect(result.success).toBe(true) + if (result.success) { + expect(result.data instanceof RecordId || result.data instanceof StringRecordId).toBe(true) + if (result.data instanceof RecordId) { + expect(result.data.tb).toBe('temperature') + expect(result.data.id).toBe(JSON.stringify(objId)) + } else { + expect(result.data.rid).toBe(`temperature:${JSON.stringify(objId)}`) + } + } }) - test('Invalid record ID (not a valid string format)', () => { + test('Valid object with tb and id', () => { const schema = recordId() - const result = schema.safeParse('invalidstring') - expect(result.success).toBe(false) - }) - - test('Valid RecordId with specific table', () => { - const schema = recordId('internet') - const result = schema.safeParse(createRecordId('internet', 'test')) - expect(result.success).toBe(true) - }) - - test('Valid StringRecordId with specific table', () => { - const schema = recordId('internet') - const result = schema.safeParse(createStringRecordId('internet', 'test')) + const result = schema.safeParse({ tb: 'internet', id: 9000 }) expect(result.success).toBe(true) + if (result.success) { + expect(result.data instanceof RecordId || result.data instanceof StringRecordId).toBe(true) + if (result.data instanceof RecordId) { + expect(result.data.tb).toBe('internet') + expect(result.data.id).toBe(9000) + } else { + expect(result.data.rid).toBe('internet:9000') + } + } }) - test('Valid string with specific table (transformed to StringRecordId)', () => { - const schema = recordId('internet') - const result = schema.safeParse('internet:test') + test('Valid object with rid', () => { + const schema = recordId() + const result = schema.safeParse({ rid: 'internet:9000' }) expect(result.success).toBe(true) if (result.success) { - expect(result.data).toBeInstanceOf(StringRecordId) - expect((result.data as StringRecordId).rid).toBe('internet:test') + expect(result.data instanceof RecordId || result.data instanceof StringRecordId).toBe(true) + if (result.data instanceof RecordId) { + expect(result.data.tb).toBe('internet') + expect(result.data.id).toBe('9000') + } else { + expect(result.data.rid).toBe('internet:9000') + } } }) + test('Invalid record ID (not a valid string format)', () => { + const schema = recordId() + const result = schema.safeParse('invalidstring') + expect(result.success).toBe(false) + }) + test('Invalid RecordId with wrong table', () => { const schema = recordId('internet') const result = schema.safeParse(createRecordId('users', 'test')) diff --git a/src/genSchema/ensureRecordSchema.ts b/src/genSchema/ensureRecordSchema.ts index 0f34bf2..080e738 100644 --- a/src/genSchema/ensureRecordSchema.ts +++ b/src/genSchema/ensureRecordSchema.ts @@ -3,37 +3,71 @@ import { join } from 'node:path' import { mkdirp } from 'mkdirp' export const ensureRecordSchema = async (rootPath: string) => { - const content = `import z from 'zod'; + const content = `import z from 'zod' import { RecordId, StringRecordId } from 'surrealdb' -type TableRecordId = RecordId | StringRecordId | \`\${T}:\${string}\`; +const RecordIdValue = z.union([z.string(), z.number(), z.bigint(), z.record(z.unknown()), z.array(z.unknown())]) + +type RecordIdValue = z.infer export function recordId
(table?: Table) { - const tableRegex = table ? table : '[A-Za-z_][A-Za-z0-9_]*'; - const idRegex = '[^:]+'; - const fullRegex = new RegExp(\`^\${tableRegex}:\${idRegex}$\`); - - return z.union([ - z.custom>((val): val is RecordId => val instanceof RecordId) - .refine((val): val is RecordId
=> !table || val.tb === table, { - message: table ? \`RecordId must be of type '\${table}'\` : undefined - }), - z.custom((val): val is StringRecordId => val instanceof StringRecordId) - .refine((val) => !table || val.rid.startsWith(\`\${table}:\`), { - message: table ? \`StringRecordId must start with '\${table}:'\` : undefined - }), - z.string().regex(fullRegex, { - message: table - ? \`Invalid record ID format. Must be '\${table}:id'\` - : "Invalid record ID format. Must be 'table:id'" - }) - ]).transform((val): TableRecordId
=> { - if (typeof val === 'string') { - return new StringRecordId(val) as TableRecordId
; - } - return val as TableRecordId
; - }); -}` + const tableRegex = table ? table : '[A-Za-z_][A-Za-z0-9_]*' + const idRegex = '[^:]+' + const fullRegex = new RegExp(\`^\${tableRegex}:\${idRegex}$\`) + + return z + .union([ + z + .custom>((val): val is RecordId => val instanceof RecordId) + .refine((val): val is RecordId
=> !table || val.tb === table, { + message: table ? \`RecordId must be of type '\${table}'\` : undefined, + }), + z + .custom((val): val is StringRecordId => val instanceof StringRecordId) + .refine(val => !table || val.rid.startsWith(\`\${table}:\`), { + message: table ? \`StringRecordId must start with '\${table}:'\` : undefined, + }), + z.string().regex(fullRegex, { + message: table + ? \`Invalid record ID format. Must be '\${table}:id'\` + : "Invalid record ID format. Must be 'table:id'", + }), + z.object({ + rid: z.string().regex(fullRegex), + }), + z + .object({ + tb: z.string(), + id: z.union([z.string(), z.number(), z.record(z.unknown())]), + }) + .refine(val => !table || val.tb === table, { + message: table ? \`RecordId must be of type '\${table}'\` : undefined, + }), + ]) + .transform((val): RecordId
| StringRecordId => { + if (val instanceof RecordId) { + return val as RecordId
+ } + if (val instanceof StringRecordId) { + return val + } + if (typeof val === 'string') { + const [tb, ...idParts] = val.split(':') + const id = idParts.join(':') + if (!tb || !id) throw new Error('Invalid record ID string format') + return new StringRecordId(val) + } + if ('rid' in val) { + const [tb, ...idParts] = val.rid.split(':') + const id = idParts.join(':') + if (!tb || !id) throw new Error('Invalid rid object format') + return new StringRecordId(val.rid) + } + if ('tb' in val && 'id' in val) { + return new RecordId(val.tb, val.id) as RecordId
+ } + throw new Error('Invalid input for RecordId') + })` await mkdirp(rootPath)