@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/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