@parsrun/entity 0.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +200 -0
- package/dist/index.js.map +1 -0
- package/dist/pg.d.ts +15 -0
- package/dist/pg.js +137 -0
- package/dist/pg.js.map +1 -0
- package/dist/sqlite.d.ts +15 -0
- package/dist/sqlite.js +132 -0
- package/dist/sqlite.js.map +1 -0
- package/dist/types-CmS0cBdC.d.ts +149 -0
- package/package.json +63 -0
- package/src/define.ts +313 -0
- package/src/index.ts +61 -0
- package/src/pg.ts +224 -0
- package/src/sqlite.ts +216 -0
- package/src/types.ts +174 -0
package/src/define.ts
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { type, type Type } from 'arktype'
|
|
2
|
+
import type {
|
|
3
|
+
EntityDefinition,
|
|
4
|
+
Field,
|
|
5
|
+
FieldDefinition,
|
|
6
|
+
Entity,
|
|
7
|
+
} from './types.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert a field definition to an ArkType type string
|
|
11
|
+
*/
|
|
12
|
+
function fieldToArkType(field: Field): string {
|
|
13
|
+
const def: FieldDefinition = typeof field === 'string'
|
|
14
|
+
? { type: field }
|
|
15
|
+
: field
|
|
16
|
+
|
|
17
|
+
let typeStr = def.type
|
|
18
|
+
|
|
19
|
+
// Handle min/max constraints
|
|
20
|
+
if (def.min !== undefined || def.max !== undefined) {
|
|
21
|
+
if (typeStr === 'string' || typeStr.startsWith('string')) {
|
|
22
|
+
if (def.min !== undefined && def.max !== undefined) {
|
|
23
|
+
typeStr = `string >= ${def.min} <= ${def.max}`
|
|
24
|
+
} else if (def.min !== undefined) {
|
|
25
|
+
typeStr = `string >= ${def.min}`
|
|
26
|
+
} else if (def.max !== undefined) {
|
|
27
|
+
typeStr = `string <= ${def.max}`
|
|
28
|
+
}
|
|
29
|
+
} else if (typeStr === 'number' || typeStr.startsWith('number')) {
|
|
30
|
+
const base = typeStr.includes('.integer') ? 'number.integer' : 'number'
|
|
31
|
+
if (def.min !== undefined && def.max !== undefined) {
|
|
32
|
+
typeStr = `${base} >= ${def.min} <= ${def.max}`
|
|
33
|
+
} else if (def.min !== undefined) {
|
|
34
|
+
typeStr = `${base} >= ${def.min}`
|
|
35
|
+
} else if (def.max !== undefined) {
|
|
36
|
+
typeStr = `${base} <= ${def.max}`
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Handle optional fields
|
|
42
|
+
if (def.optional) {
|
|
43
|
+
// For optional fields, allow undefined or the type
|
|
44
|
+
typeStr = `${typeStr} | undefined`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return typeStr
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the full schema object for ArkType
|
|
52
|
+
*/
|
|
53
|
+
function buildSchemaObject(
|
|
54
|
+
definition: EntityDefinition<Record<string, Field>>,
|
|
55
|
+
mode: 'full' | 'create' | 'update' | 'query'
|
|
56
|
+
): Record<string, string> {
|
|
57
|
+
const schema: Record<string, string> = {}
|
|
58
|
+
|
|
59
|
+
// Add id field for full schema
|
|
60
|
+
if (mode === 'full') {
|
|
61
|
+
schema['id'] = 'string.uuid'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Add tenantId if tenant-scoped
|
|
65
|
+
if (definition.tenant) {
|
|
66
|
+
if (mode === 'full' || mode === 'create') {
|
|
67
|
+
schema['tenantId'] = 'string.uuid'
|
|
68
|
+
}
|
|
69
|
+
if (mode === 'query') {
|
|
70
|
+
schema['tenantId?'] = 'string.uuid'
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Add user-defined fields
|
|
75
|
+
for (const [name, field] of Object.entries(definition.fields)) {
|
|
76
|
+
const def: FieldDefinition = typeof field === 'string'
|
|
77
|
+
? { type: field }
|
|
78
|
+
: field
|
|
79
|
+
|
|
80
|
+
if (mode === 'update' || mode === 'query') {
|
|
81
|
+
// All fields optional for update/query
|
|
82
|
+
schema[`${name}?`] = fieldToArkType(field)
|
|
83
|
+
} else if (mode === 'create') {
|
|
84
|
+
// Skip fields with defaults or optional fields
|
|
85
|
+
if (def.default !== undefined || def.optional) {
|
|
86
|
+
schema[`${name}?`] = fieldToArkType(field)
|
|
87
|
+
} else {
|
|
88
|
+
schema[name] = fieldToArkType(field)
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
// Full schema
|
|
92
|
+
if (def.optional) {
|
|
93
|
+
schema[`${name}?`] = fieldToArkType(field)
|
|
94
|
+
} else {
|
|
95
|
+
schema[name] = fieldToArkType(field)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Add timestamp fields
|
|
101
|
+
if (definition.timestamps) {
|
|
102
|
+
if (mode === 'full') {
|
|
103
|
+
schema['insertedAt'] = 'Date'
|
|
104
|
+
schema['updatedAt'] = 'Date'
|
|
105
|
+
}
|
|
106
|
+
if (mode === 'query') {
|
|
107
|
+
schema['insertedAt?'] = 'Date'
|
|
108
|
+
schema['updatedAt?'] = 'Date'
|
|
109
|
+
schema['insertedAfter?'] = 'Date'
|
|
110
|
+
schema['insertedBefore?'] = 'Date'
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Add soft delete field
|
|
115
|
+
if (definition.softDelete) {
|
|
116
|
+
if (mode === 'full') {
|
|
117
|
+
schema['deletedAt?'] = 'Date'
|
|
118
|
+
}
|
|
119
|
+
if (mode === 'query') {
|
|
120
|
+
schema['includeDeleted?'] = 'boolean'
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Add pagination for query
|
|
125
|
+
if (mode === 'query') {
|
|
126
|
+
schema['limit?'] = 'number.integer > 0'
|
|
127
|
+
schema['offset?'] = 'number.integer >= 0'
|
|
128
|
+
schema['cursor?'] = 'string'
|
|
129
|
+
schema['orderBy?'] = 'string'
|
|
130
|
+
schema['orderDirection?'] = "'asc' | 'desc'"
|
|
131
|
+
schema['search?'] = 'string'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return schema
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Define an entity with single-source schema generation
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* const Product = defineEntity({
|
|
143
|
+
* name: 'products',
|
|
144
|
+
* tenant: true,
|
|
145
|
+
* timestamps: true,
|
|
146
|
+
* softDelete: true,
|
|
147
|
+
* fields: {
|
|
148
|
+
* name: 'string >= 1',
|
|
149
|
+
* price: { type: 'number', min: 0 },
|
|
150
|
+
* status: "'draft' | 'active' | 'archived'",
|
|
151
|
+
* },
|
|
152
|
+
* indexes: [
|
|
153
|
+
* { fields: ['tenantId', 'status'] },
|
|
154
|
+
* ],
|
|
155
|
+
* })
|
|
156
|
+
*
|
|
157
|
+
* // Use schemas
|
|
158
|
+
* const validated = Product.createSchema(input)
|
|
159
|
+
* const products = await db.select().from(Product.table)
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export function defineEntity<
|
|
163
|
+
TName extends string,
|
|
164
|
+
TFields extends Record<string, Field>,
|
|
165
|
+
>(
|
|
166
|
+
definition: EntityDefinition<TFields> & { name: TName }
|
|
167
|
+
): Entity<TName, TFields, Record<string, unknown>> {
|
|
168
|
+
// Build schema objects
|
|
169
|
+
const fullSchemaObj = buildSchemaObject(definition, 'full')
|
|
170
|
+
const createSchemaObj = buildSchemaObject(definition, 'create')
|
|
171
|
+
const updateSchemaObj = buildSchemaObject(definition, 'update')
|
|
172
|
+
const querySchemaObj = buildSchemaObject(definition, 'query')
|
|
173
|
+
|
|
174
|
+
// Create ArkType schemas
|
|
175
|
+
const schema = type(fullSchemaObj as Record<string, string>)
|
|
176
|
+
const createSchema = type(createSchemaObj as Record<string, string>)
|
|
177
|
+
const updateSchema = type(updateSchemaObj as Record<string, string>)
|
|
178
|
+
const querySchema = type(querySchemaObj as Record<string, string>)
|
|
179
|
+
|
|
180
|
+
// Determine auto and required fields
|
|
181
|
+
const autoFields: string[] = ['id']
|
|
182
|
+
if (definition.timestamps) {
|
|
183
|
+
autoFields.push('insertedAt', 'updatedAt')
|
|
184
|
+
}
|
|
185
|
+
if (definition.softDelete) {
|
|
186
|
+
autoFields.push('deletedAt')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const requiredFields: string[] = []
|
|
190
|
+
const optionalFields: string[] = []
|
|
191
|
+
|
|
192
|
+
if (definition.tenant) {
|
|
193
|
+
requiredFields.push('tenantId')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const [name, field] of Object.entries(definition.fields)) {
|
|
197
|
+
const def: FieldDefinition = typeof field === 'string'
|
|
198
|
+
? { type: field }
|
|
199
|
+
: field
|
|
200
|
+
|
|
201
|
+
if (def.optional || def.default !== undefined) {
|
|
202
|
+
optionalFields.push(name)
|
|
203
|
+
} else {
|
|
204
|
+
requiredFields.push(name)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
name: definition.name as TName,
|
|
210
|
+
definition,
|
|
211
|
+
schema: schema as Type<Record<string, unknown>>,
|
|
212
|
+
createSchema: createSchema as Type<Record<string, unknown>>,
|
|
213
|
+
updateSchema: updateSchema as Type<Record<string, unknown>>,
|
|
214
|
+
querySchema: querySchema as Type<Record<string, unknown>>,
|
|
215
|
+
infer: {} as Record<string, unknown>,
|
|
216
|
+
autoFields,
|
|
217
|
+
requiredFields,
|
|
218
|
+
optionalFields,
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Create a reference field to another entity
|
|
224
|
+
*/
|
|
225
|
+
export function ref(
|
|
226
|
+
entity: string | { name: string },
|
|
227
|
+
options?: {
|
|
228
|
+
field?: string
|
|
229
|
+
onDelete?: 'cascade' | 'set null' | 'restrict' | 'no action'
|
|
230
|
+
optional?: boolean
|
|
231
|
+
}
|
|
232
|
+
): FieldDefinition {
|
|
233
|
+
const entityName = typeof entity === 'string' ? entity : entity.name
|
|
234
|
+
const result: FieldDefinition = {
|
|
235
|
+
type: 'string.uuid',
|
|
236
|
+
db: {
|
|
237
|
+
references: {
|
|
238
|
+
entity: entityName,
|
|
239
|
+
field: options?.field ?? 'id',
|
|
240
|
+
onDelete: options?.onDelete ?? 'restrict',
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
if (options?.optional !== undefined) {
|
|
245
|
+
result.optional = options.optional
|
|
246
|
+
}
|
|
247
|
+
return result
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Create an enum field from a list of values
|
|
252
|
+
*/
|
|
253
|
+
export function enumField<T extends string>(
|
|
254
|
+
values: readonly T[],
|
|
255
|
+
options?: { default?: T; optional?: boolean }
|
|
256
|
+
): FieldDefinition {
|
|
257
|
+
const typeStr = values.map(v => `'${v}'`).join(' | ')
|
|
258
|
+
const result: FieldDefinition = {
|
|
259
|
+
type: typeStr as `'${string}'`,
|
|
260
|
+
}
|
|
261
|
+
if (options?.default !== undefined) {
|
|
262
|
+
result.default = options.default
|
|
263
|
+
}
|
|
264
|
+
if (options?.optional !== undefined) {
|
|
265
|
+
result.optional = options.optional
|
|
266
|
+
}
|
|
267
|
+
return result
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Create a JSON field
|
|
272
|
+
*/
|
|
273
|
+
export function jsonField<T = Record<string, unknown>>(
|
|
274
|
+
options?: { optional?: boolean; default?: T }
|
|
275
|
+
): FieldDefinition {
|
|
276
|
+
const result: FieldDefinition = {
|
|
277
|
+
type: 'json',
|
|
278
|
+
}
|
|
279
|
+
if (options?.optional !== undefined) {
|
|
280
|
+
result.optional = options.optional
|
|
281
|
+
}
|
|
282
|
+
if (options?.default !== undefined) {
|
|
283
|
+
result.default = options.default
|
|
284
|
+
}
|
|
285
|
+
return result
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Create a decimal field with precision
|
|
290
|
+
*/
|
|
291
|
+
export function decimal(
|
|
292
|
+
precision: number,
|
|
293
|
+
scale: number,
|
|
294
|
+
options?: { min?: number; max?: number; optional?: boolean }
|
|
295
|
+
): FieldDefinition {
|
|
296
|
+
const result: FieldDefinition = {
|
|
297
|
+
type: 'number',
|
|
298
|
+
db: {
|
|
299
|
+
precision,
|
|
300
|
+
scale,
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
if (options?.min !== undefined) {
|
|
304
|
+
result.min = options.min
|
|
305
|
+
}
|
|
306
|
+
if (options?.max !== undefined) {
|
|
307
|
+
result.max = options.max
|
|
308
|
+
}
|
|
309
|
+
if (options?.optional !== undefined) {
|
|
310
|
+
result.optional = options.optional
|
|
311
|
+
}
|
|
312
|
+
return result
|
|
313
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @parsrun/entity
|
|
3
|
+
*
|
|
4
|
+
* Single-source entity definitions for Pars framework.
|
|
5
|
+
* Define once, generate ArkType schemas and Drizzle tables.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { defineEntity, ref, enumField } from '@parsrun/entity'
|
|
10
|
+
* import { toPgTable } from '@parsrun/entity/pg'
|
|
11
|
+
*
|
|
12
|
+
* // Define entity
|
|
13
|
+
* const Product = defineEntity({
|
|
14
|
+
* name: 'products',
|
|
15
|
+
* tenant: true,
|
|
16
|
+
* timestamps: true,
|
|
17
|
+
* softDelete: true,
|
|
18
|
+
* fields: {
|
|
19
|
+
* name: 'string >= 1',
|
|
20
|
+
* slug: 'string',
|
|
21
|
+
* description: { type: 'string', optional: true },
|
|
22
|
+
* price: { type: 'number', min: 0 },
|
|
23
|
+
* stock: { type: 'number.integer', min: 0, default: 0 },
|
|
24
|
+
* status: enumField(['draft', 'active', 'archived'], { default: 'draft' }),
|
|
25
|
+
* categoryId: ref('categories', { onDelete: 'set null', optional: true }),
|
|
26
|
+
* },
|
|
27
|
+
* indexes: [
|
|
28
|
+
* { fields: ['tenantId', 'slug'], unique: true },
|
|
29
|
+
* { fields: ['tenantId', 'status'] },
|
|
30
|
+
* ],
|
|
31
|
+
* })
|
|
32
|
+
*
|
|
33
|
+
* // Use ArkType schemas
|
|
34
|
+
* const input = Product.createSchema(requestBody)
|
|
35
|
+
* if (input instanceof type.errors) throw new ValidationError(input)
|
|
36
|
+
*
|
|
37
|
+
* // Generate Drizzle table
|
|
38
|
+
* const productsTable = toPgTable(Product)
|
|
39
|
+
* await db.insert(productsTable).values({ tenantId, ...input })
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
export { defineEntity, ref, enumField, jsonField, decimal } from './define.js'
|
|
44
|
+
|
|
45
|
+
export type {
|
|
46
|
+
FieldType,
|
|
47
|
+
FieldDefinition,
|
|
48
|
+
SimpleFieldDefinition,
|
|
49
|
+
Field,
|
|
50
|
+
IndexDefinition,
|
|
51
|
+
EntityDefinition,
|
|
52
|
+
EntitySchemas,
|
|
53
|
+
Entity,
|
|
54
|
+
DrizzleOptions,
|
|
55
|
+
InferEntity,
|
|
56
|
+
InferCreateInput,
|
|
57
|
+
InferUpdateInput,
|
|
58
|
+
} from './types.js'
|
|
59
|
+
|
|
60
|
+
// Re-export arktype type function for convenience
|
|
61
|
+
export { type } from 'arktype'
|
package/src/pg.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Drizzle table generation from entity definitions
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
pgTable,
|
|
6
|
+
uuid,
|
|
7
|
+
text,
|
|
8
|
+
integer,
|
|
9
|
+
boolean,
|
|
10
|
+
timestamp,
|
|
11
|
+
numeric,
|
|
12
|
+
json,
|
|
13
|
+
index,
|
|
14
|
+
uniqueIndex,
|
|
15
|
+
} from 'drizzle-orm/pg-core'
|
|
16
|
+
import { sql } from 'drizzle-orm'
|
|
17
|
+
import type { Entity, Field, FieldDefinition } from './types.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert field definition to Drizzle column builder
|
|
21
|
+
*/
|
|
22
|
+
function fieldToColumn(name: string, field: Field) {
|
|
23
|
+
const def: FieldDefinition = typeof field === 'string' ? { type: field } : field
|
|
24
|
+
|
|
25
|
+
const columnName = def.db?.column ?? toSnakeCase(name)
|
|
26
|
+
let column: any
|
|
27
|
+
|
|
28
|
+
// Determine column type based on field type
|
|
29
|
+
const fieldType = def.type.split(' ')[0] ?? 'string'
|
|
30
|
+
|
|
31
|
+
switch (fieldType) {
|
|
32
|
+
case 'string.uuid':
|
|
33
|
+
column = uuid(columnName)
|
|
34
|
+
break
|
|
35
|
+
|
|
36
|
+
case 'string.email':
|
|
37
|
+
case 'string.url':
|
|
38
|
+
case 'string':
|
|
39
|
+
column = text(columnName)
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
case 'number.integer':
|
|
43
|
+
column = integer(columnName)
|
|
44
|
+
break
|
|
45
|
+
|
|
46
|
+
case 'number':
|
|
47
|
+
if (def.db?.precision) {
|
|
48
|
+
column = numeric(columnName, {
|
|
49
|
+
precision: def.db.precision,
|
|
50
|
+
scale: def.db.scale ?? 2,
|
|
51
|
+
})
|
|
52
|
+
} else {
|
|
53
|
+
column = numeric(columnName)
|
|
54
|
+
}
|
|
55
|
+
break
|
|
56
|
+
|
|
57
|
+
case 'boolean':
|
|
58
|
+
column = boolean(columnName)
|
|
59
|
+
break
|
|
60
|
+
|
|
61
|
+
case 'Date':
|
|
62
|
+
column = timestamp(columnName, { withTimezone: true })
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
case 'json':
|
|
66
|
+
column = json(columnName)
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
default:
|
|
70
|
+
// Union types like "'draft' | 'active'" become text
|
|
71
|
+
column = text(columnName)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Apply modifiers
|
|
75
|
+
if (!def.optional && def.default === undefined) {
|
|
76
|
+
column = column.notNull()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (def.default !== undefined) {
|
|
80
|
+
column = column.default(def.default)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return column
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Convert camelCase to snake_case
|
|
88
|
+
*/
|
|
89
|
+
function toSnakeCase(str: string): string {
|
|
90
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate a Drizzle pgTable from an entity definition
|
|
95
|
+
*/
|
|
96
|
+
export function toPgTable<E extends Entity<string, Record<string, Field>, unknown>>(
|
|
97
|
+
entity: E,
|
|
98
|
+
tableRefs?: Record<string, any>
|
|
99
|
+
): any {
|
|
100
|
+
const def = entity.definition
|
|
101
|
+
const columns: Record<string, any> = {}
|
|
102
|
+
|
|
103
|
+
// Add id column
|
|
104
|
+
columns['id'] = uuid('id').primaryKey().defaultRandom()
|
|
105
|
+
|
|
106
|
+
// Add tenantId if tenant-scoped
|
|
107
|
+
if (def.tenant) {
|
|
108
|
+
const tenantsTable = tableRefs?.['tenants']
|
|
109
|
+
if (tenantsTable) {
|
|
110
|
+
columns['tenantId'] = uuid('tenant_id')
|
|
111
|
+
.notNull()
|
|
112
|
+
.references(() => tenantsTable.id, { onDelete: 'cascade' })
|
|
113
|
+
} else {
|
|
114
|
+
columns['tenantId'] = uuid('tenant_id').notNull()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Add user-defined fields
|
|
119
|
+
for (const [name, field] of Object.entries(def.fields)) {
|
|
120
|
+
const fieldDef: FieldDefinition = typeof field === 'string' ? { type: field } : field
|
|
121
|
+
|
|
122
|
+
// Handle references
|
|
123
|
+
if (fieldDef.db?.references && tableRefs) {
|
|
124
|
+
const refTable = tableRefs[fieldDef.db.references.entity]
|
|
125
|
+
if (refTable) {
|
|
126
|
+
const refField = fieldDef.db.references.field ?? 'id'
|
|
127
|
+
const onDelete = fieldDef.db.references.onDelete ?? 'restrict'
|
|
128
|
+
|
|
129
|
+
let col = uuid(toSnakeCase(name)).references(() => refTable[refField], { onDelete })
|
|
130
|
+
if (!fieldDef.optional) {
|
|
131
|
+
col = col.notNull()
|
|
132
|
+
}
|
|
133
|
+
columns[name] = col
|
|
134
|
+
} else {
|
|
135
|
+
columns[name] = fieldToColumn(name, field)
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
columns[name] = fieldToColumn(name, field)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Add timestamp fields
|
|
143
|
+
if (def.timestamps) {
|
|
144
|
+
columns['insertedAt'] = timestamp('inserted_at', { withTimezone: true })
|
|
145
|
+
.notNull()
|
|
146
|
+
.defaultNow()
|
|
147
|
+
|
|
148
|
+
columns['updatedAt'] = timestamp('updated_at', { withTimezone: true })
|
|
149
|
+
.notNull()
|
|
150
|
+
.defaultNow()
|
|
151
|
+
.$onUpdate(() => new Date())
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Add soft delete field
|
|
155
|
+
if (def.softDelete) {
|
|
156
|
+
columns['deletedAt'] = timestamp('deleted_at', { withTimezone: true })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Create table with indexes
|
|
160
|
+
return pgTable(def.name, columns, (t: any) => {
|
|
161
|
+
const indexes: Record<string, any> = {}
|
|
162
|
+
|
|
163
|
+
// Add indexes from definition
|
|
164
|
+
if (def.indexes) {
|
|
165
|
+
for (const idx of def.indexes) {
|
|
166
|
+
const idxName = idx.name ?? `${def.name}_${idx.fields.join('_')}_idx`
|
|
167
|
+
const idxColumns = idx.fields.map((f) => t[f]).filter(Boolean)
|
|
168
|
+
|
|
169
|
+
if (idxColumns.length === 0) continue
|
|
170
|
+
|
|
171
|
+
if (idx.unique) {
|
|
172
|
+
if (idx.where) {
|
|
173
|
+
indexes[idxName] = uniqueIndex(idxName)
|
|
174
|
+
.on(...(idxColumns as [any, ...any[]]))
|
|
175
|
+
.where(sql.raw(idx.where))
|
|
176
|
+
} else {
|
|
177
|
+
indexes[idxName] = uniqueIndex(idxName).on(...(idxColumns as [any, ...any[]]))
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
if (idx.where) {
|
|
181
|
+
indexes[idxName] = index(idxName)
|
|
182
|
+
.on(...(idxColumns as [any, ...any[]]))
|
|
183
|
+
.where(sql.raw(idx.where))
|
|
184
|
+
} else {
|
|
185
|
+
indexes[idxName] = index(idxName).on(...(idxColumns as [any, ...any[]]))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Add default tenant index if tenant-scoped
|
|
192
|
+
if (def.tenant && t.tenantId) {
|
|
193
|
+
const tenantIdxName = `${def.name}_tenant_id_idx`
|
|
194
|
+
if (!indexes[tenantIdxName]) {
|
|
195
|
+
indexes[tenantIdxName] = index(tenantIdxName).on(t.tenantId)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return indexes
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Create multiple tables with resolved references
|
|
205
|
+
*/
|
|
206
|
+
export function createPgSchema<
|
|
207
|
+
T extends Record<string, Entity<string, Record<string, Field>, unknown>>,
|
|
208
|
+
>(entities: T): { [K in keyof T]: any } {
|
|
209
|
+
const tables: Record<string, any> = {}
|
|
210
|
+
|
|
211
|
+
// First pass: create tables without references
|
|
212
|
+
for (const [key, entity] of Object.entries(entities)) {
|
|
213
|
+
tables[key] = toPgTable(entity)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Second pass: recreate tables with resolved references
|
|
217
|
+
for (const [key, entity] of Object.entries(entities)) {
|
|
218
|
+
tables[key] = toPgTable(entity, tables)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return tables as { [K in keyof T]: any }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export { toPgTable as toTable }
|