@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/sqlite.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Drizzle table generation from entity definitions
|
|
3
|
+
* Compatible with Cloudflare D1
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
sqliteTable,
|
|
7
|
+
text,
|
|
8
|
+
integer,
|
|
9
|
+
real,
|
|
10
|
+
index,
|
|
11
|
+
uniqueIndex,
|
|
12
|
+
} from 'drizzle-orm/sqlite-core'
|
|
13
|
+
import { sql } from 'drizzle-orm'
|
|
14
|
+
import type { Entity, Field, FieldDefinition } from './types.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert field definition to Drizzle SQLite column builder
|
|
18
|
+
*/
|
|
19
|
+
function fieldToColumn(name: string, field: Field) {
|
|
20
|
+
const def: FieldDefinition = typeof field === 'string' ? { type: field } : field
|
|
21
|
+
|
|
22
|
+
const columnName = toSnakeCase(name)
|
|
23
|
+
let column: any
|
|
24
|
+
|
|
25
|
+
// Determine column type based on field type
|
|
26
|
+
const fieldType = def.type.split(' ')[0] ?? 'string'
|
|
27
|
+
|
|
28
|
+
switch (fieldType) {
|
|
29
|
+
case 'string.uuid':
|
|
30
|
+
case 'string.email':
|
|
31
|
+
case 'string.url':
|
|
32
|
+
case 'string':
|
|
33
|
+
column = text(columnName)
|
|
34
|
+
break
|
|
35
|
+
|
|
36
|
+
case 'number.integer':
|
|
37
|
+
column = integer(columnName)
|
|
38
|
+
break
|
|
39
|
+
|
|
40
|
+
case 'number':
|
|
41
|
+
column = real(columnName)
|
|
42
|
+
break
|
|
43
|
+
|
|
44
|
+
case 'boolean':
|
|
45
|
+
column = integer(columnName, { mode: 'boolean' })
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
case 'Date':
|
|
49
|
+
column = text(columnName)
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
case 'json':
|
|
53
|
+
column = text(columnName, { mode: 'json' })
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
default:
|
|
57
|
+
column = text(columnName)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Apply modifiers
|
|
61
|
+
if (!def.optional && def.default === undefined) {
|
|
62
|
+
column = column.notNull()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (def.default !== undefined) {
|
|
66
|
+
if (typeof def.default === 'object') {
|
|
67
|
+
column = column.default(JSON.stringify(def.default))
|
|
68
|
+
} else {
|
|
69
|
+
column = column.default(def.default)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return column
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convert camelCase to snake_case
|
|
78
|
+
*/
|
|
79
|
+
function toSnakeCase(str: string): string {
|
|
80
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generate a Drizzle sqliteTable from an entity definition
|
|
85
|
+
*/
|
|
86
|
+
export function toSqliteTable<E extends Entity<string, Record<string, Field>, unknown>>(
|
|
87
|
+
entity: E,
|
|
88
|
+
tableRefs?: Record<string, any>
|
|
89
|
+
): any {
|
|
90
|
+
const def = entity.definition
|
|
91
|
+
const columns: Record<string, any> = {}
|
|
92
|
+
|
|
93
|
+
// Add id column
|
|
94
|
+
columns['id'] = text('id')
|
|
95
|
+
.primaryKey()
|
|
96
|
+
.$defaultFn(() => crypto.randomUUID())
|
|
97
|
+
|
|
98
|
+
// Add tenantId if tenant-scoped
|
|
99
|
+
if (def.tenant) {
|
|
100
|
+
const tenantsTable = tableRefs?.['tenants']
|
|
101
|
+
if (tenantsTable) {
|
|
102
|
+
columns['tenantId'] = text('tenant_id')
|
|
103
|
+
.notNull()
|
|
104
|
+
.references(() => tenantsTable.id, { onDelete: 'cascade' })
|
|
105
|
+
} else {
|
|
106
|
+
columns['tenantId'] = text('tenant_id').notNull()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Add user-defined fields
|
|
111
|
+
for (const [name, field] of Object.entries(def.fields)) {
|
|
112
|
+
const fieldDef: FieldDefinition = typeof field === 'string' ? { type: field } : field
|
|
113
|
+
|
|
114
|
+
// Handle references
|
|
115
|
+
if (fieldDef.db?.references && tableRefs) {
|
|
116
|
+
const refTable = tableRefs[fieldDef.db.references.entity]
|
|
117
|
+
if (refTable) {
|
|
118
|
+
const refField = fieldDef.db.references.field ?? 'id'
|
|
119
|
+
const onDelete = fieldDef.db.references.onDelete ?? 'restrict'
|
|
120
|
+
|
|
121
|
+
let col = text(toSnakeCase(name)).references(() => refTable[refField], { onDelete })
|
|
122
|
+
if (!fieldDef.optional) {
|
|
123
|
+
col = col.notNull()
|
|
124
|
+
}
|
|
125
|
+
columns[name] = col
|
|
126
|
+
} else {
|
|
127
|
+
columns[name] = fieldToColumn(name, field)
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
columns[name] = fieldToColumn(name, field)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Add timestamp fields
|
|
135
|
+
if (def.timestamps) {
|
|
136
|
+
columns['insertedAt'] = text('inserted_at')
|
|
137
|
+
.notNull()
|
|
138
|
+
.$defaultFn(() => new Date().toISOString())
|
|
139
|
+
|
|
140
|
+
columns['updatedAt'] = text('updated_at')
|
|
141
|
+
.notNull()
|
|
142
|
+
.$defaultFn(() => new Date().toISOString())
|
|
143
|
+
.$onUpdate(() => new Date().toISOString())
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Add soft delete field
|
|
147
|
+
if (def.softDelete) {
|
|
148
|
+
columns['deletedAt'] = text('deleted_at')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Create table with indexes
|
|
152
|
+
return sqliteTable(def.name, columns, (t: any) => {
|
|
153
|
+
const indexes: Record<string, any> = {}
|
|
154
|
+
|
|
155
|
+
// Add indexes from definition
|
|
156
|
+
if (def.indexes) {
|
|
157
|
+
for (const idx of def.indexes) {
|
|
158
|
+
const idxName = idx.name ?? `${def.name}_${idx.fields.join('_')}_idx`
|
|
159
|
+
const idxColumns = idx.fields.map((f) => t[f]).filter(Boolean)
|
|
160
|
+
|
|
161
|
+
if (idxColumns.length === 0) continue
|
|
162
|
+
|
|
163
|
+
if (idx.unique) {
|
|
164
|
+
if (idx.where) {
|
|
165
|
+
indexes[idxName] = uniqueIndex(idxName)
|
|
166
|
+
.on(...(idxColumns as [any, ...any[]]))
|
|
167
|
+
.where(sql.raw(idx.where))
|
|
168
|
+
} else {
|
|
169
|
+
indexes[idxName] = uniqueIndex(idxName).on(...(idxColumns as [any, ...any[]]))
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
if (idx.where) {
|
|
173
|
+
indexes[idxName] = index(idxName)
|
|
174
|
+
.on(...(idxColumns as [any, ...any[]]))
|
|
175
|
+
.where(sql.raw(idx.where))
|
|
176
|
+
} else {
|
|
177
|
+
indexes[idxName] = index(idxName).on(...(idxColumns as [any, ...any[]]))
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Add default tenant index if tenant-scoped
|
|
184
|
+
if (def.tenant && t.tenantId) {
|
|
185
|
+
const tenantIdxName = `${def.name}_tenant_id_idx`
|
|
186
|
+
if (!indexes[tenantIdxName]) {
|
|
187
|
+
indexes[tenantIdxName] = index(tenantIdxName).on(t.tenantId)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return indexes
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Create multiple SQLite tables with resolved references
|
|
197
|
+
*/
|
|
198
|
+
export function createSqliteSchema<
|
|
199
|
+
T extends Record<string, Entity<string, Record<string, Field>, unknown>>,
|
|
200
|
+
>(entities: T): { [K in keyof T]: any } {
|
|
201
|
+
const tables: Record<string, any> = {}
|
|
202
|
+
|
|
203
|
+
// First pass: create tables without references
|
|
204
|
+
for (const [key, entity] of Object.entries(entities)) {
|
|
205
|
+
tables[key] = toSqliteTable(entity)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Second pass: recreate tables with resolved references
|
|
209
|
+
for (const [key, entity] of Object.entries(entities)) {
|
|
210
|
+
tables[key] = toSqliteTable(entity, tables)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return tables as { [K in keyof T]: any }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export { toSqliteTable as toTable }
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Type } from 'arktype'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Supported field types for entity definitions
|
|
5
|
+
*/
|
|
6
|
+
export type FieldType =
|
|
7
|
+
| 'string'
|
|
8
|
+
| 'string.uuid'
|
|
9
|
+
| 'string.email'
|
|
10
|
+
| 'string.url'
|
|
11
|
+
| 'number'
|
|
12
|
+
| 'number.integer'
|
|
13
|
+
| 'boolean'
|
|
14
|
+
| 'Date'
|
|
15
|
+
| 'json'
|
|
16
|
+
| `'${string}'` // Literal string
|
|
17
|
+
| `'${string}' | '${string}'` // Union of literals (expanded below)
|
|
18
|
+
| string // For complex arktype expressions
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Field definition with full options
|
|
22
|
+
*/
|
|
23
|
+
export interface FieldDefinition {
|
|
24
|
+
/** The arktype type string */
|
|
25
|
+
type: FieldType
|
|
26
|
+
/** Whether the field is optional (nullable in DB) */
|
|
27
|
+
optional?: boolean
|
|
28
|
+
/** Default value for the field */
|
|
29
|
+
default?: unknown
|
|
30
|
+
/** Minimum value/length */
|
|
31
|
+
min?: number
|
|
32
|
+
/** Maximum value/length */
|
|
33
|
+
max?: number
|
|
34
|
+
/** Regex pattern for strings */
|
|
35
|
+
pattern?: RegExp
|
|
36
|
+
/** Database-specific options */
|
|
37
|
+
db?: {
|
|
38
|
+
/** Custom column name */
|
|
39
|
+
column?: string
|
|
40
|
+
/** Precision for decimals */
|
|
41
|
+
precision?: number
|
|
42
|
+
/** Scale for decimals */
|
|
43
|
+
scale?: number
|
|
44
|
+
/** Whether to index this field */
|
|
45
|
+
index?: boolean
|
|
46
|
+
/** Whether this field is unique (within tenant if multi-tenant) */
|
|
47
|
+
unique?: boolean
|
|
48
|
+
/** Reference to another entity */
|
|
49
|
+
references?: {
|
|
50
|
+
entity: string
|
|
51
|
+
field?: string
|
|
52
|
+
onDelete?: 'cascade' | 'set null' | 'restrict' | 'no action'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Simplified field definition - just the type string
|
|
59
|
+
*/
|
|
60
|
+
export type SimpleFieldDefinition = FieldType
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Field can be either simple or full definition
|
|
64
|
+
*/
|
|
65
|
+
export type Field = SimpleFieldDefinition | FieldDefinition
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Index definition
|
|
69
|
+
*/
|
|
70
|
+
export interface IndexDefinition {
|
|
71
|
+
/** Fields to include in the index */
|
|
72
|
+
fields: string[]
|
|
73
|
+
/** Whether the index is unique */
|
|
74
|
+
unique?: boolean
|
|
75
|
+
/** Optional name for the index */
|
|
76
|
+
name?: string
|
|
77
|
+
/** SQL WHERE clause for partial index */
|
|
78
|
+
where?: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Entity definition options
|
|
83
|
+
*/
|
|
84
|
+
export interface EntityDefinition<TFields extends Record<string, Field>> {
|
|
85
|
+
/** Table name in the database */
|
|
86
|
+
name: string
|
|
87
|
+
/** Human-readable description */
|
|
88
|
+
description?: string
|
|
89
|
+
/** Whether this entity is tenant-scoped (adds tenantId field) */
|
|
90
|
+
tenant?: boolean
|
|
91
|
+
/** Field definitions */
|
|
92
|
+
fields: TFields
|
|
93
|
+
/** Index definitions */
|
|
94
|
+
indexes?: IndexDefinition[]
|
|
95
|
+
/** Whether to add timestamp fields (insertedAt, updatedAt) */
|
|
96
|
+
timestamps?: boolean
|
|
97
|
+
/** Whether to add soft delete (deletedAt) */
|
|
98
|
+
softDelete?: boolean
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generated schemas from entity definition
|
|
103
|
+
*/
|
|
104
|
+
export interface EntitySchemas<T> {
|
|
105
|
+
/** Full entity schema (all fields) */
|
|
106
|
+
schema: Type<T>
|
|
107
|
+
/** Schema for creating (without id, timestamps) */
|
|
108
|
+
createSchema: Type<Partial<T>>
|
|
109
|
+
/** Schema for updating (all fields optional) */
|
|
110
|
+
updateSchema: Type<Partial<T>>
|
|
111
|
+
/** Schema for query filters */
|
|
112
|
+
querySchema: Type<Record<string, unknown>>
|
|
113
|
+
/** Schema for list response with pagination */
|
|
114
|
+
listSchema: Type<{ items: T[]; total: number; nextCursor?: string }>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The complete entity object returned by defineEntity
|
|
119
|
+
*/
|
|
120
|
+
export interface Entity<
|
|
121
|
+
TName extends string,
|
|
122
|
+
TFields extends Record<string, Field>,
|
|
123
|
+
TType,
|
|
124
|
+
> {
|
|
125
|
+
/** Entity name (table name) */
|
|
126
|
+
name: TName
|
|
127
|
+
/** Original definition */
|
|
128
|
+
definition: EntityDefinition<TFields>
|
|
129
|
+
/** ArkType schema for the full entity */
|
|
130
|
+
schema: Type<TType>
|
|
131
|
+
/** ArkType schema for create operations */
|
|
132
|
+
createSchema: Type<Partial<TType>>
|
|
133
|
+
/** ArkType schema for update operations */
|
|
134
|
+
updateSchema: Type<Partial<TType>>
|
|
135
|
+
/** ArkType schema for query/filter operations */
|
|
136
|
+
querySchema: Type<Record<string, unknown>>
|
|
137
|
+
/** TypeScript type (use typeof entity.infer) */
|
|
138
|
+
infer: TType
|
|
139
|
+
/** Field names that are auto-generated (id, timestamps) */
|
|
140
|
+
autoFields: string[]
|
|
141
|
+
/** Field names that are required for creation */
|
|
142
|
+
requiredFields: string[]
|
|
143
|
+
/** Field names that are optional */
|
|
144
|
+
optionalFields: string[]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Options for Drizzle table generation
|
|
149
|
+
*/
|
|
150
|
+
export interface DrizzleOptions {
|
|
151
|
+
/** Database dialect */
|
|
152
|
+
dialect: 'pg' | 'sqlite' | 'mysql'
|
|
153
|
+
/** Custom schema name (PostgreSQL) */
|
|
154
|
+
schema?: string
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Utility type to extract the inferred type from an entity
|
|
159
|
+
*/
|
|
160
|
+
export type InferEntity<E> = E extends Entity<string, Record<string, Field>, infer T> ? T : never
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Utility type to extract create input type
|
|
164
|
+
*/
|
|
165
|
+
export type InferCreateInput<E> = E extends Entity<string, Record<string, Field>, infer T>
|
|
166
|
+
? Omit<T, 'id' | 'insertedAt' | 'updatedAt' | 'deletedAt'>
|
|
167
|
+
: never
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Utility type to extract update input type
|
|
171
|
+
*/
|
|
172
|
+
export type InferUpdateInput<E> = E extends Entity<string, Record<string, Field>, infer T>
|
|
173
|
+
? Partial<Omit<T, 'id' | 'tenantId' | 'insertedAt' | 'updatedAt' | 'deletedAt'>>
|
|
174
|
+
: never
|