Skip to content

Commit

Permalink
Merge pull request #68 from sebastianwessel/data-type-improvements
Browse files Browse the repository at this point in the history
Record Id improvements
  • Loading branch information
sebastianwessel authored Nov 21, 2024
2 parents 287dc3a + 3fd647e commit 3b8e4c4
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 61 deletions.
163 changes: 129 additions & 34 deletions src/genSchema/ensureRecordSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ const RecordIdValue = z.union([z.string(), z.number(), z.bigint(), z.record(z.un

type RecordIdValue = z.infer<typeof RecordIdValue>

type TableRecordId<T extends string> = RecordId<T> | StringRecordId | `${T}:${string}`

function recordId<Table extends string = string>(table?: Table) {
const tableRegex = table ? table : '[A-Za-z_][A-Za-z0-9_]*'
const idRegex = '[^:]+'
Expand All @@ -30,40 +28,93 @@ function recordId<Table extends string = string>(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<Table> => {
.transform((val): RecordId<Table> | StringRecordId => {
if (val instanceof RecordId) {
return val as RecordId<Table>
}
if (val instanceof StringRecordId) {
return val
}
if (typeof val === 'string') {
return new StringRecordId(val) as TableRecordId<Table>
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<Table>
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<Table>
}
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: <explanation>
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')
}
}
})

Expand All @@ -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'))
Expand Down
88 changes: 61 additions & 27 deletions src/genSchema/ensureRecordSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends string> = RecordId<T> | 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<typeof RecordIdValue>
export function recordId<Table extends string = string>(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<RecordId<string>>((val): val is RecordId<string> => val instanceof RecordId)
.refine((val): val is RecordId<Table> => !table || val.tb === table, {
message: table ? \`RecordId must be of type '\${table}'\` : undefined
}),
z.custom<StringRecordId>((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<Table> => {
if (typeof val === 'string') {
return new StringRecordId(val) as TableRecordId<Table>;
}
return val as TableRecordId<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<RecordId<string>>((val): val is RecordId<string> => val instanceof RecordId)
.refine((val): val is RecordId<Table> => !table || val.tb === table, {
message: table ? \`RecordId must be of type '\${table}'\` : undefined,
}),
z
.custom<StringRecordId>((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<Table> | StringRecordId => {
if (val instanceof RecordId) {
return val as RecordId<Table>
}
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<Table>
}
throw new Error('Invalid input for RecordId')
})`

await mkdirp(rootPath)

Expand Down

0 comments on commit 3b8e4c4

Please sign in to comment.