appflare 0.2.14 → 0.2.16
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/Documentation.md +27 -0
- package/cli/schema-compiler.ts +535 -9
- package/cli/templates/dashboard/builders/table-routes/helpers.ts +1 -5
- package/cli/templates/handlers/generators/types/query-definitions/filter-and-where-types.ts +88 -7
- package/cli/templates/handlers/generators/types/query-definitions/query-api-types.ts +1 -1
- package/cli/templates/handlers/generators/types/query-definitions/query-helper-functions.ts +360 -11
- package/cli/templates/handlers/generators/types/query-definitions/schema-and-table-types.ts +174 -19
- package/cli/templates/handlers/generators/types/query-runtime/runtime-aggregate-and-footer.ts +14 -4
- package/cli/templates/handlers/generators/types/query-runtime/runtime-read.ts +52 -16
- package/cli/templates/handlers/generators/types/query-runtime/runtime-write.ts +425 -6
- package/cli/utils/schema-discovery.ts +1 -1
- package/dist/cli/index.js +1478 -385
- package/dist/cli/index.mjs +1478 -385
- package/dist/index.d.mts +31 -2
- package/dist/index.d.ts +31 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/schema.ts +76 -3
package/Documentation.md
CHANGED
|
@@ -45,6 +45,33 @@ Schema is defined with `schema`, `table`, and `v` from Appflare.
|
|
|
45
45
|
|
|
46
46
|
Example source: `packages/backend/schema.ts`.
|
|
47
47
|
|
|
48
|
+
### 2.3 Relation helpers (`v.one`, `v.many`, `v.manyToMany`)
|
|
49
|
+
|
|
50
|
+
- `v.one("target")` creates a single-reference relation and infers a local FK field.
|
|
51
|
+
- `v.many("target")` is inverse one-to-many and infers an FK on the target table.
|
|
52
|
+
- `v.manyToMany("target")` creates a many-to-many relation by synthesizing a junction table.
|
|
53
|
+
|
|
54
|
+
Many-to-many example:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
export const schemas = schema({
|
|
58
|
+
pets: table({
|
|
59
|
+
id: v.uuid(),
|
|
60
|
+
trips: v.manyToMany("trips"),
|
|
61
|
+
}),
|
|
62
|
+
trips: table({
|
|
63
|
+
id: v.uuid(),
|
|
64
|
+
pets: v.manyToMany("pets"),
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Default behavior for `v.manyToMany`:
|
|
70
|
+
|
|
71
|
+
- Generates one deterministic junction table per pair.
|
|
72
|
+
- Junction rows are pure links (two FK columns, no payload columns).
|
|
73
|
+
- Reciprocal declarations must agree on options (`junctionTable`, field names, FK actions), otherwise generation throws a conflict error.
|
|
74
|
+
|
|
48
75
|
---
|
|
49
76
|
|
|
50
77
|
## 3) How to create handlers
|
package/cli/schema-compiler.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ColumnDefinition,
|
|
6
6
|
ColumnType,
|
|
7
7
|
ManyRelationDefinition,
|
|
8
|
+
ManyToManyRelationDefinition,
|
|
8
9
|
OneRelationDefinition,
|
|
9
10
|
SchemaDefinition,
|
|
10
11
|
TableDefinition,
|
|
@@ -121,6 +122,8 @@ function ensureInferredReferenceColumn(
|
|
|
121
122
|
fkType?: ColumnType;
|
|
122
123
|
sqlName?: string;
|
|
123
124
|
notNull?: boolean;
|
|
125
|
+
onDelete?: OneRelationDefinition["onDelete"];
|
|
126
|
+
onUpdate?: OneRelationDefinition["onUpdate"];
|
|
124
127
|
},
|
|
125
128
|
inferredType: ColumnType,
|
|
126
129
|
): void {
|
|
@@ -138,7 +141,17 @@ function ensureInferredReferenceColumn(
|
|
|
138
141
|
|
|
139
142
|
table.columns[columnName] = {
|
|
140
143
|
...existing,
|
|
141
|
-
|
|
144
|
+
notNull:
|
|
145
|
+
existing.notNull ??
|
|
146
|
+
(existing.nullable === true ? false : options.notNull),
|
|
147
|
+
nullable:
|
|
148
|
+
existing.nullable ?? (options.notNull === false ? true : undefined),
|
|
149
|
+
references: {
|
|
150
|
+
table: referenceTable,
|
|
151
|
+
column: referenceField,
|
|
152
|
+
onDelete: existing.references?.onDelete ?? options.onDelete,
|
|
153
|
+
onUpdate: existing.references?.onUpdate ?? options.onUpdate,
|
|
154
|
+
},
|
|
142
155
|
};
|
|
143
156
|
return;
|
|
144
157
|
}
|
|
@@ -148,16 +161,360 @@ function ensureInferredReferenceColumn(
|
|
|
148
161
|
type: options.fkType ?? inferredType,
|
|
149
162
|
sqlName: options.sqlName,
|
|
150
163
|
notNull: options.notNull ?? true,
|
|
164
|
+
nullable: options.notNull === false ? true : undefined,
|
|
151
165
|
references: {
|
|
152
166
|
table: referenceTable,
|
|
153
167
|
column: referenceField,
|
|
168
|
+
onDelete: options.onDelete,
|
|
169
|
+
onUpdate: options.onUpdate,
|
|
154
170
|
},
|
|
155
171
|
};
|
|
156
172
|
}
|
|
157
173
|
|
|
174
|
+
function resolveNotNullFromNullableFlags(
|
|
175
|
+
fieldPath: string,
|
|
176
|
+
flags: { notNull?: boolean; nullable?: boolean },
|
|
177
|
+
defaultNotNull: boolean,
|
|
178
|
+
): boolean {
|
|
179
|
+
if (flags.notNull === true && flags.nullable === true) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Invalid nullable configuration on '${fieldPath}': cannot set both notNull and nullable to true.`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (flags.notNull === true) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (flags.nullable === true) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (flags.notNull === false) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return defaultNotNull;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
type ManyToManyPair = {
|
|
201
|
+
leftTable: string;
|
|
202
|
+
leftReferenceField: string;
|
|
203
|
+
rightTable: string;
|
|
204
|
+
rightReferenceField: string;
|
|
205
|
+
junctionTable: string;
|
|
206
|
+
leftField: string;
|
|
207
|
+
rightField: string;
|
|
208
|
+
leftSqlName?: string;
|
|
209
|
+
rightSqlName?: string;
|
|
210
|
+
onDelete?: OneRelationDefinition["onDelete"];
|
|
211
|
+
onUpdate?: OneRelationDefinition["onUpdate"];
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
function endpointKey(tableName: string, referenceField: string): string {
|
|
215
|
+
return `${tableName}:${referenceField}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function normalizeManyToManyPairKey(
|
|
219
|
+
sourceTable: string,
|
|
220
|
+
sourceReferenceField: string,
|
|
221
|
+
targetTable: string,
|
|
222
|
+
targetReferenceField: string,
|
|
223
|
+
): {
|
|
224
|
+
key: string;
|
|
225
|
+
leftTable: string;
|
|
226
|
+
leftReferenceField: string;
|
|
227
|
+
rightTable: string;
|
|
228
|
+
rightReferenceField: string;
|
|
229
|
+
sourceIsLeft: boolean;
|
|
230
|
+
} {
|
|
231
|
+
const source = endpointKey(sourceTable, sourceReferenceField);
|
|
232
|
+
const target = endpointKey(targetTable, targetReferenceField);
|
|
233
|
+
const sourceIsLeft = source <= target;
|
|
234
|
+
|
|
235
|
+
if (sourceIsLeft) {
|
|
236
|
+
return {
|
|
237
|
+
key: `${source}|${target}`,
|
|
238
|
+
leftTable: sourceTable,
|
|
239
|
+
leftReferenceField: sourceReferenceField,
|
|
240
|
+
rightTable: targetTable,
|
|
241
|
+
rightReferenceField: targetReferenceField,
|
|
242
|
+
sourceIsLeft: true,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
key: `${target}|${source}`,
|
|
248
|
+
leftTable: targetTable,
|
|
249
|
+
leftReferenceField: targetReferenceField,
|
|
250
|
+
rightTable: sourceTable,
|
|
251
|
+
rightReferenceField: sourceReferenceField,
|
|
252
|
+
sourceIsLeft: false,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function defaultManyToManyJunctionTable(
|
|
257
|
+
leftTable: string,
|
|
258
|
+
rightTable: string,
|
|
259
|
+
): string {
|
|
260
|
+
return `${leftTable}${toPascalCase(rightTable)}Links`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function defaultManyToManyFieldName(
|
|
264
|
+
ownerTable: string,
|
|
265
|
+
otherTable: string,
|
|
266
|
+
suffix: "source" | "target",
|
|
267
|
+
): string {
|
|
268
|
+
const ownerSingular = singularize(ownerTable);
|
|
269
|
+
const otherSingular = singularize(otherTable);
|
|
270
|
+
|
|
271
|
+
if (ownerSingular === otherSingular) {
|
|
272
|
+
return `${suffix}${toPascalCase(ownerSingular)}Id`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return `${ownerSingular}Id`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function mergeManyToManyPairConfig(
|
|
279
|
+
existing: ManyToManyPair,
|
|
280
|
+
incoming: ManyToManyPair,
|
|
281
|
+
pairKey: string,
|
|
282
|
+
): ManyToManyPair {
|
|
283
|
+
if (existing.junctionTable !== incoming.junctionTable) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`manyToMany pair '${pairKey}' has conflicting junctionTable values ('${existing.junctionTable}' vs '${incoming.junctionTable}').`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (existing.leftField !== incoming.leftField) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`manyToMany pair '${pairKey}' has conflicting left field values ('${existing.leftField}' vs '${incoming.leftField}').`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (existing.rightField !== incoming.rightField) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`manyToMany pair '${pairKey}' has conflicting right field values ('${existing.rightField}' vs '${incoming.rightField}').`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (existing.leftSqlName !== incoming.leftSqlName) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`manyToMany pair '${pairKey}' has conflicting left sql name values ('${existing.leftSqlName}' vs '${incoming.leftSqlName}').`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (existing.rightSqlName !== incoming.rightSqlName) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`manyToMany pair '${pairKey}' has conflicting right sql name values ('${existing.rightSqlName}' vs '${incoming.rightSqlName}').`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (existing.onDelete !== incoming.onDelete) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`manyToMany pair '${pairKey}' has conflicting onDelete values ('${existing.onDelete}' vs '${incoming.onDelete}').`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (existing.onUpdate !== incoming.onUpdate) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`manyToMany pair '${pairKey}' has conflicting onUpdate values ('${existing.onUpdate}' vs '${incoming.onUpdate}').`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return existing;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function normalizeManyToManyRelations(definition: SchemaDefinition): void {
|
|
329
|
+
const pairMap = new Map<string, ManyToManyPair>();
|
|
330
|
+
|
|
331
|
+
for (const [sourceTableName, sourceTable] of Object.entries(
|
|
332
|
+
definition.tables,
|
|
333
|
+
)) {
|
|
334
|
+
for (const relation of Object.values(sourceTable.relations)) {
|
|
335
|
+
if (relation.relation !== "manyToMany") {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const sourceReferenceField = relation.referenceField ?? "id";
|
|
340
|
+
const targetReferenceField = relation.targetReferenceField ?? "id";
|
|
341
|
+
|
|
342
|
+
const normalizedPair = normalizeManyToManyPairKey(
|
|
343
|
+
sourceTableName,
|
|
344
|
+
sourceReferenceField,
|
|
345
|
+
relation.targetTable,
|
|
346
|
+
targetReferenceField,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const defaultLeftField = defaultManyToManyFieldName(
|
|
350
|
+
normalizedPair.leftTable,
|
|
351
|
+
normalizedPair.rightTable,
|
|
352
|
+
"source",
|
|
353
|
+
);
|
|
354
|
+
const defaultRightField = defaultManyToManyFieldName(
|
|
355
|
+
normalizedPair.rightTable,
|
|
356
|
+
normalizedPair.leftTable,
|
|
357
|
+
"target",
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const pair: ManyToManyPair = {
|
|
361
|
+
leftTable: normalizedPair.leftTable,
|
|
362
|
+
leftReferenceField: normalizedPair.leftReferenceField,
|
|
363
|
+
rightTable: normalizedPair.rightTable,
|
|
364
|
+
rightReferenceField: normalizedPair.rightReferenceField,
|
|
365
|
+
junctionTable:
|
|
366
|
+
relation.junctionTable ??
|
|
367
|
+
defaultManyToManyJunctionTable(
|
|
368
|
+
normalizedPair.leftTable,
|
|
369
|
+
normalizedPair.rightTable,
|
|
370
|
+
),
|
|
371
|
+
leftField: normalizedPair.sourceIsLeft
|
|
372
|
+
? (relation.sourceField ?? defaultLeftField)
|
|
373
|
+
: (relation.targetField ?? defaultLeftField),
|
|
374
|
+
rightField: normalizedPair.sourceIsLeft
|
|
375
|
+
? (relation.targetField ?? defaultRightField)
|
|
376
|
+
: (relation.sourceField ?? defaultRightField),
|
|
377
|
+
leftSqlName: normalizedPair.sourceIsLeft
|
|
378
|
+
? relation.sourceSqlName
|
|
379
|
+
: relation.targetSqlName,
|
|
380
|
+
rightSqlName: normalizedPair.sourceIsLeft
|
|
381
|
+
? relation.targetSqlName
|
|
382
|
+
: relation.sourceSqlName,
|
|
383
|
+
onDelete: relation.onDelete,
|
|
384
|
+
onUpdate: relation.onUpdate,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
if (pair.leftField === pair.rightField) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
`manyToMany pair '${normalizedPair.key}' resolves to duplicate junction fields '${pair.leftField}'. Set sourceField/targetField explicitly.`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const existing = pairMap.get(normalizedPair.key);
|
|
394
|
+
if (existing) {
|
|
395
|
+
mergeManyToManyPairConfig(existing, pair, normalizedPair.key);
|
|
396
|
+
} else {
|
|
397
|
+
pairMap.set(normalizedPair.key, pair);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
relation.referenceField = sourceReferenceField;
|
|
401
|
+
relation.targetReferenceField = targetReferenceField;
|
|
402
|
+
relation.junctionTable = pair.junctionTable;
|
|
403
|
+
relation.sourceField = normalizedPair.sourceIsLeft
|
|
404
|
+
? pair.leftField
|
|
405
|
+
: pair.rightField;
|
|
406
|
+
relation.targetField = normalizedPair.sourceIsLeft
|
|
407
|
+
? pair.rightField
|
|
408
|
+
: pair.leftField;
|
|
409
|
+
relation.sourceSqlName = normalizedPair.sourceIsLeft
|
|
410
|
+
? pair.leftSqlName
|
|
411
|
+
: pair.rightSqlName;
|
|
412
|
+
relation.targetSqlName = normalizedPair.sourceIsLeft
|
|
413
|
+
? pair.rightSqlName
|
|
414
|
+
: pair.leftSqlName;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
for (const pair of pairMap.values()) {
|
|
419
|
+
if (pair.junctionTable in definition.tables) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`manyToMany auto junction table '${pair.junctionTable}' conflicts with an existing table. Set a different junctionTable name.`,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const leftType =
|
|
426
|
+
getLocalReferenceType(
|
|
427
|
+
definition,
|
|
428
|
+
pair.leftTable,
|
|
429
|
+
pair.leftReferenceField,
|
|
430
|
+
) ?? "string";
|
|
431
|
+
const rightType =
|
|
432
|
+
getLocalReferenceType(
|
|
433
|
+
definition,
|
|
434
|
+
pair.rightTable,
|
|
435
|
+
pair.rightReferenceField,
|
|
436
|
+
) ?? "string";
|
|
437
|
+
|
|
438
|
+
definition.tables[pair.junctionTable] = {
|
|
439
|
+
kind: "table",
|
|
440
|
+
columns: {
|
|
441
|
+
[pair.leftField]: {
|
|
442
|
+
kind: "column",
|
|
443
|
+
type: leftType,
|
|
444
|
+
sqlName: pair.leftSqlName,
|
|
445
|
+
notNull: true,
|
|
446
|
+
nullable: false,
|
|
447
|
+
references: {
|
|
448
|
+
table: pair.leftTable,
|
|
449
|
+
column: pair.leftReferenceField,
|
|
450
|
+
onDelete: pair.onDelete,
|
|
451
|
+
onUpdate: pair.onUpdate,
|
|
452
|
+
},
|
|
453
|
+
index: true,
|
|
454
|
+
},
|
|
455
|
+
[pair.rightField]: {
|
|
456
|
+
kind: "column",
|
|
457
|
+
type: rightType,
|
|
458
|
+
sqlName: pair.rightSqlName,
|
|
459
|
+
notNull: true,
|
|
460
|
+
nullable: false,
|
|
461
|
+
references: {
|
|
462
|
+
table: pair.rightTable,
|
|
463
|
+
column: pair.rightReferenceField,
|
|
464
|
+
onDelete: pair.onDelete,
|
|
465
|
+
onUpdate: pair.onUpdate,
|
|
466
|
+
},
|
|
467
|
+
index: true,
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
relations: {
|
|
471
|
+
[pair.leftTable]: {
|
|
472
|
+
kind: "relation",
|
|
473
|
+
relation: "one",
|
|
474
|
+
targetTable: pair.leftTable,
|
|
475
|
+
field: pair.leftField,
|
|
476
|
+
referenceField: pair.leftReferenceField,
|
|
477
|
+
},
|
|
478
|
+
[pair.rightTable]: {
|
|
479
|
+
kind: "relation",
|
|
480
|
+
relation: "one",
|
|
481
|
+
targetTable: pair.rightTable,
|
|
482
|
+
field: pair.rightField,
|
|
483
|
+
referenceField: pair.rightReferenceField,
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function validateNullableFlags(definition: SchemaDefinition): void {
|
|
491
|
+
for (const [tableName, table] of Object.entries(definition.tables)) {
|
|
492
|
+
for (const [columnName, column] of Object.entries(table.columns)) {
|
|
493
|
+
if (column.notNull === true && column.nullable === true) {
|
|
494
|
+
throw new Error(
|
|
495
|
+
`Invalid nullable configuration on '${tableName}.${columnName}': cannot set both notNull and nullable to true.`,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
for (const [relationName, relation] of Object.entries(table.relations)) {
|
|
501
|
+
if (
|
|
502
|
+
relation.relation !== "manyToMany" &&
|
|
503
|
+
relation.notNull === true &&
|
|
504
|
+
relation.nullable === true
|
|
505
|
+
) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`Invalid nullable configuration on '${tableName}.${relationName}': cannot set both notNull and nullable to true.`,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
158
514
|
function normalizeSchemaDefinition(
|
|
159
515
|
definition: SchemaDefinition,
|
|
160
516
|
): SchemaDefinition {
|
|
517
|
+
validateNullableFlags(definition);
|
|
161
518
|
const normalized = cloneSchemaDefinition(definition);
|
|
162
519
|
|
|
163
520
|
for (const [tableName, table] of Object.entries(normalized.tables)) {
|
|
@@ -188,7 +545,13 @@ function normalizeSchemaDefinition(
|
|
|
188
545
|
{
|
|
189
546
|
fkType: relation.fkType,
|
|
190
547
|
sqlName: relation.sqlName,
|
|
191
|
-
notNull:
|
|
548
|
+
notNull: resolveNotNullFromNullableFlags(
|
|
549
|
+
`${tableName}.${relationName}`,
|
|
550
|
+
relation,
|
|
551
|
+
true,
|
|
552
|
+
),
|
|
553
|
+
onDelete: relation.onDelete,
|
|
554
|
+
onUpdate: relation.onUpdate,
|
|
192
555
|
},
|
|
193
556
|
inferredType,
|
|
194
557
|
);
|
|
@@ -211,6 +574,7 @@ function normalizeSchemaDefinition(
|
|
|
211
574
|
const referenceField = relation.referenceField ?? "id";
|
|
212
575
|
const inferredField =
|
|
213
576
|
relation.field ?? `${singularize(sourceTableName)}Id`;
|
|
577
|
+
relation.field = inferredField;
|
|
214
578
|
const inferredType =
|
|
215
579
|
getLocalReferenceType(normalized, sourceTableName, referenceField) ??
|
|
216
580
|
relation.fkType ??
|
|
@@ -225,18 +589,27 @@ function normalizeSchemaDefinition(
|
|
|
225
589
|
{
|
|
226
590
|
fkType: relation.fkType,
|
|
227
591
|
sqlName: relation.sqlName,
|
|
228
|
-
notNull:
|
|
592
|
+
notNull: resolveNotNullFromNullableFlags(
|
|
593
|
+
`${sourceTableName}.${relation.targetTable}`,
|
|
594
|
+
relation,
|
|
595
|
+
true,
|
|
596
|
+
),
|
|
597
|
+
onDelete: relation.onDelete,
|
|
598
|
+
onUpdate: relation.onUpdate,
|
|
229
599
|
},
|
|
230
600
|
inferredType,
|
|
231
601
|
);
|
|
232
602
|
}
|
|
233
603
|
}
|
|
234
604
|
|
|
605
|
+
normalizeManyToManyRelations(normalized);
|
|
606
|
+
|
|
235
607
|
return normalized;
|
|
236
608
|
}
|
|
237
609
|
|
|
238
610
|
function isOptionalInsertColumn(column: ColumnDefinition): boolean {
|
|
239
611
|
return (
|
|
612
|
+
column.primaryKey === true ||
|
|
240
613
|
column.notNull !== true ||
|
|
241
614
|
column.autoIncrement === true ||
|
|
242
615
|
column.sqlDefault !== undefined ||
|
|
@@ -357,6 +730,117 @@ function resolveColumnReference(
|
|
|
357
730
|
};
|
|
358
731
|
}
|
|
359
732
|
|
|
733
|
+
function emitManyToManyRuntimeMetadata(definition: SchemaDefinition): string {
|
|
734
|
+
const tableEntries: string[] = [];
|
|
735
|
+
|
|
736
|
+
for (const [tableName, table] of Object.entries(definition.tables)) {
|
|
737
|
+
const relationEntries: string[] = [];
|
|
738
|
+
|
|
739
|
+
for (const [relationName, relation] of Object.entries(table.relations)) {
|
|
740
|
+
if (relation.relation !== "manyToMany") {
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (!relation.junctionTable) {
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
relationEntries.push(
|
|
749
|
+
`${quote(relationName)}: {
|
|
750
|
+
targetTable: ${quote(relation.targetTable)},
|
|
751
|
+
junctionTable: ${quote(relation.junctionTable)},
|
|
752
|
+
sourceField: ${quote(relation.sourceField ?? "")},
|
|
753
|
+
targetField: ${quote(relation.targetField ?? "")},
|
|
754
|
+
},`,
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (relationEntries.length === 0) {
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
tableEntries.push(
|
|
763
|
+
`${quote(tableName)}: {
|
|
764
|
+
${relationEntries.map((entry) => `\t${entry}`).join("\n")}
|
|
765
|
+
},`,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (tableEntries.length === 0) {
|
|
770
|
+
return "export const __appflareManyToMany = {} as const;\n";
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return `export const __appflareManyToMany = {
|
|
774
|
+
${tableEntries.map((entry) => `\t${entry}`).join("\n")}
|
|
775
|
+
} as const;
|
|
776
|
+
`;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function emitRuntimeRelationMetadata(definition: SchemaDefinition): string {
|
|
780
|
+
const tableEntries: string[] = [];
|
|
781
|
+
|
|
782
|
+
for (const [tableName, table] of Object.entries(definition.tables)) {
|
|
783
|
+
const relationEntries: string[] = [];
|
|
784
|
+
|
|
785
|
+
for (const [relationName, relation] of Object.entries(table.relations)) {
|
|
786
|
+
if (relation.relation === "one") {
|
|
787
|
+
relationEntries.push(
|
|
788
|
+
`${quote(relationName)}: {
|
|
789
|
+
kind: "one",
|
|
790
|
+
targetTable: ${quote(relation.targetTable)},
|
|
791
|
+
sourceField: ${quote(relation.field ?? "")},
|
|
792
|
+
referenceField: ${quote(relation.referenceField ?? "id")},
|
|
793
|
+
},`,
|
|
794
|
+
);
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (relation.relation === "many") {
|
|
799
|
+
relationEntries.push(
|
|
800
|
+
`${quote(relationName)}: {
|
|
801
|
+
kind: "many",
|
|
802
|
+
targetTable: ${quote(relation.targetTable)},
|
|
803
|
+
sourceField: ${quote(relation.field ?? "")},
|
|
804
|
+
referenceField: ${quote(relation.referenceField ?? "id")},
|
|
805
|
+
},`,
|
|
806
|
+
);
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
relationEntries.push(
|
|
811
|
+
`${quote(relationName)}: {
|
|
812
|
+
kind: "manyToMany",
|
|
813
|
+
targetTable: ${quote(relation.targetTable)},
|
|
814
|
+
junctionTable: ${quote(relation.junctionTable ?? "")},
|
|
815
|
+
sourceField: ${quote(relation.sourceField ?? "")},
|
|
816
|
+
targetField: ${quote(relation.targetField ?? "")},
|
|
817
|
+
referenceField: ${quote(relation.referenceField ?? "id")},
|
|
818
|
+
targetReferenceField: ${quote(relation.targetReferenceField ?? "id")},
|
|
819
|
+
},`,
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (relationEntries.length === 0) {
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
tableEntries.push(
|
|
828
|
+
`${quote(tableName)}: {
|
|
829
|
+
${relationEntries.map((entry) => `\t${entry}`).join("\n")}
|
|
830
|
+
},`,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (tableEntries.length === 0) {
|
|
835
|
+
return "export const __appflareRelations = {} as const;\n";
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return `export const __appflareRelations = {
|
|
839
|
+
${tableEntries.map((entry) => `\t${entry}`).join("\n")}
|
|
840
|
+
} as const;
|
|
841
|
+
`;
|
|
842
|
+
}
|
|
843
|
+
|
|
360
844
|
function emitDrizzleSchema(
|
|
361
845
|
definition: SchemaDefinition,
|
|
362
846
|
strategy: "camelToSnake",
|
|
@@ -388,6 +872,10 @@ function emitDrizzleSchema(
|
|
|
388
872
|
for (const [fieldName, column] of Object.entries(table.columns)) {
|
|
389
873
|
let expr = drizzleBaseColumn(fieldName, column, strategy);
|
|
390
874
|
|
|
875
|
+
if (column.uuidPrimaryKey) {
|
|
876
|
+
expr += ".$defaultFn(() => crypto.randomUUID())";
|
|
877
|
+
}
|
|
878
|
+
|
|
391
879
|
if (column.primaryKey) {
|
|
392
880
|
expr += column.autoIncrement
|
|
393
881
|
? ".primaryKey({ autoIncrement: true })"
|
|
@@ -402,7 +890,18 @@ function emitDrizzleSchema(
|
|
|
402
890
|
|
|
403
891
|
const reference = resolveColumnReference(fieldName, column, table);
|
|
404
892
|
if (reference) {
|
|
405
|
-
|
|
893
|
+
if (column.references?.onDelete || column.references?.onUpdate) {
|
|
894
|
+
const actions: string[] = [];
|
|
895
|
+
if (column.references.onDelete) {
|
|
896
|
+
actions.push(`onDelete: ${quote(column.references.onDelete)}`);
|
|
897
|
+
}
|
|
898
|
+
if (column.references.onUpdate) {
|
|
899
|
+
actions.push(`onUpdate: ${quote(column.references.onUpdate)}`);
|
|
900
|
+
}
|
|
901
|
+
expr += `.references(() => ${reference.tableName}.${reference.fieldName}, { ${actions.join(", ")} })`;
|
|
902
|
+
} else {
|
|
903
|
+
expr += `.references(() => ${reference.tableName}.${reference.fieldName})`;
|
|
904
|
+
}
|
|
406
905
|
}
|
|
407
906
|
|
|
408
907
|
if (column.unique) {
|
|
@@ -446,8 +945,17 @@ function emitDrizzleSchema(
|
|
|
446
945
|
return relation.relation === "many";
|
|
447
946
|
},
|
|
448
947
|
) as Array<[string, ManyRelationDefinition]>;
|
|
948
|
+
const manyToManyRelations = Object.entries(table.relations).filter(
|
|
949
|
+
([, relation]) => {
|
|
950
|
+
return relation.relation === "manyToMany";
|
|
951
|
+
},
|
|
952
|
+
) as Array<[string, ManyToManyRelationDefinition]>;
|
|
449
953
|
|
|
450
|
-
if (
|
|
954
|
+
if (
|
|
955
|
+
oneRelations.length === 0 &&
|
|
956
|
+
manyRelations.length === 0 &&
|
|
957
|
+
manyToManyRelations.length === 0
|
|
958
|
+
) {
|
|
451
959
|
continue;
|
|
452
960
|
}
|
|
453
961
|
|
|
@@ -461,6 +969,14 @@ function emitDrizzleSchema(
|
|
|
461
969
|
for (const [relationName, relation] of manyRelations) {
|
|
462
970
|
relationLines.push(`\t${relationName}: many(${relation.targetTable}),`);
|
|
463
971
|
}
|
|
972
|
+
for (const [relationName, relation] of manyToManyRelations) {
|
|
973
|
+
if (!relation.junctionTable) {
|
|
974
|
+
throw new Error(
|
|
975
|
+
`manyToMany relation '${tableName}.${relationName}' is missing junctionTable after normalization.`,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
relationLines.push(`\t${relationName}: many(${relation.junctionTable}),`);
|
|
979
|
+
}
|
|
464
980
|
|
|
465
981
|
relationBlocks.push(
|
|
466
982
|
`export const ${tableName}Relations = relations(${tableName}, ({ one, many }) => ({\n${relationLines.join("\n")}\n}));`,
|
|
@@ -474,12 +990,17 @@ ${buildExternalTableImportLines(externalTables)}
|
|
|
474
990
|
${tableBlocks.join("\n\n")}
|
|
475
991
|
|
|
476
992
|
${relationBlocks.join("\n\n")}
|
|
993
|
+
|
|
994
|
+
${emitManyToManyRuntimeMetadata(definition)}
|
|
995
|
+
|
|
996
|
+
${emitRuntimeRelationMetadata(definition)}
|
|
477
997
|
`;
|
|
478
998
|
}
|
|
479
999
|
|
|
480
1000
|
function zodSchemaExpression(
|
|
481
1001
|
column: ColumnDefinition,
|
|
482
1002
|
optional: boolean,
|
|
1003
|
+
nullable: boolean,
|
|
483
1004
|
): string {
|
|
484
1005
|
let expr = "z.unknown()";
|
|
485
1006
|
if (column.type === "int") {
|
|
@@ -499,6 +1020,10 @@ function zodSchemaExpression(
|
|
|
499
1020
|
expr += ".optional()";
|
|
500
1021
|
}
|
|
501
1022
|
|
|
1023
|
+
if (nullable) {
|
|
1024
|
+
expr += ".nullable()";
|
|
1025
|
+
}
|
|
1026
|
+
|
|
502
1027
|
return expr;
|
|
503
1028
|
}
|
|
504
1029
|
|
|
@@ -512,10 +1037,10 @@ function emitZodSchemas(definition: SchemaDefinition): string {
|
|
|
512
1037
|
|
|
513
1038
|
for (const [fieldName, column] of Object.entries(table.columns)) {
|
|
514
1039
|
insertLines.push(
|
|
515
|
-
`\t${fieldName}: ${zodSchemaExpression(column, isOptionalInsertColumn(column))},`,
|
|
1040
|
+
`\t${fieldName}: ${zodSchemaExpression(column, isOptionalInsertColumn(column), isNullableSelectColumn(column))},`,
|
|
516
1041
|
);
|
|
517
1042
|
selectLines.push(
|
|
518
|
-
`\t${fieldName}: ${zodSchemaExpression(column, isNullableSelectColumn(column))},`,
|
|
1043
|
+
`\t${fieldName}: ${zodSchemaExpression(column, isNullableSelectColumn(column), isNullableSelectColumn(column))},`,
|
|
519
1044
|
);
|
|
520
1045
|
}
|
|
521
1046
|
|
|
@@ -558,11 +1083,12 @@ function emitTypes(definition: SchemaDefinition): string {
|
|
|
558
1083
|
|
|
559
1084
|
for (const [fieldName, column] of Object.entries(table.columns)) {
|
|
560
1085
|
const tsType = toTypeScriptType(column);
|
|
1086
|
+
const nullableSuffix = isNullableSelectColumn(column) ? " | null" : "";
|
|
561
1087
|
selectFields.push(
|
|
562
|
-
`\t${fieldName}${isNullableSelectColumn(column) ? "?" : ""}: ${tsType};`,
|
|
1088
|
+
`\t${fieldName}${isNullableSelectColumn(column) ? "?" : ""}: ${tsType}${nullableSuffix};`,
|
|
563
1089
|
);
|
|
564
1090
|
insertFields.push(
|
|
565
|
-
`\t${fieldName}${isOptionalInsertColumn(column) ? "?" : ""}: ${tsType};`,
|
|
1091
|
+
`\t${fieldName}${isOptionalInsertColumn(column) ? "?" : ""}: ${tsType}${nullableSuffix};`,
|
|
566
1092
|
);
|
|
567
1093
|
}
|
|
568
1094
|
|