appflare 0.2.15 → 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 +432 -2
- 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 +172 -21
- 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/dist/cli/index.js +1476 -387
- package/dist/cli/index.mjs +1476 -387
- package/dist/index.d.mts +27 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/schema.ts +57 -1
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,
|
|
@@ -196,6 +197,296 @@ function resolveNotNullFromNullableFlags(
|
|
|
196
197
|
return defaultNotNull;
|
|
197
198
|
}
|
|
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
|
+
|
|
199
490
|
function validateNullableFlags(definition: SchemaDefinition): void {
|
|
200
491
|
for (const [tableName, table] of Object.entries(definition.tables)) {
|
|
201
492
|
for (const [columnName, column] of Object.entries(table.columns)) {
|
|
@@ -207,7 +498,11 @@ function validateNullableFlags(definition: SchemaDefinition): void {
|
|
|
207
498
|
}
|
|
208
499
|
|
|
209
500
|
for (const [relationName, relation] of Object.entries(table.relations)) {
|
|
210
|
-
if (
|
|
501
|
+
if (
|
|
502
|
+
relation.relation !== "manyToMany" &&
|
|
503
|
+
relation.notNull === true &&
|
|
504
|
+
relation.nullable === true
|
|
505
|
+
) {
|
|
211
506
|
throw new Error(
|
|
212
507
|
`Invalid nullable configuration on '${tableName}.${relationName}': cannot set both notNull and nullable to true.`,
|
|
213
508
|
);
|
|
@@ -279,6 +574,7 @@ function normalizeSchemaDefinition(
|
|
|
279
574
|
const referenceField = relation.referenceField ?? "id";
|
|
280
575
|
const inferredField =
|
|
281
576
|
relation.field ?? `${singularize(sourceTableName)}Id`;
|
|
577
|
+
relation.field = inferredField;
|
|
282
578
|
const inferredType =
|
|
283
579
|
getLocalReferenceType(normalized, sourceTableName, referenceField) ??
|
|
284
580
|
relation.fkType ??
|
|
@@ -306,6 +602,8 @@ function normalizeSchemaDefinition(
|
|
|
306
602
|
}
|
|
307
603
|
}
|
|
308
604
|
|
|
605
|
+
normalizeManyToManyRelations(normalized);
|
|
606
|
+
|
|
309
607
|
return normalized;
|
|
310
608
|
}
|
|
311
609
|
|
|
@@ -432,6 +730,117 @@ function resolveColumnReference(
|
|
|
432
730
|
};
|
|
433
731
|
}
|
|
434
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
|
+
|
|
435
844
|
function emitDrizzleSchema(
|
|
436
845
|
definition: SchemaDefinition,
|
|
437
846
|
strategy: "camelToSnake",
|
|
@@ -536,8 +945,17 @@ function emitDrizzleSchema(
|
|
|
536
945
|
return relation.relation === "many";
|
|
537
946
|
},
|
|
538
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]>;
|
|
539
953
|
|
|
540
|
-
if (
|
|
954
|
+
if (
|
|
955
|
+
oneRelations.length === 0 &&
|
|
956
|
+
manyRelations.length === 0 &&
|
|
957
|
+
manyToManyRelations.length === 0
|
|
958
|
+
) {
|
|
541
959
|
continue;
|
|
542
960
|
}
|
|
543
961
|
|
|
@@ -551,6 +969,14 @@ function emitDrizzleSchema(
|
|
|
551
969
|
for (const [relationName, relation] of manyRelations) {
|
|
552
970
|
relationLines.push(`\t${relationName}: many(${relation.targetTable}),`);
|
|
553
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
|
+
}
|
|
554
980
|
|
|
555
981
|
relationBlocks.push(
|
|
556
982
|
`export const ${tableName}Relations = relations(${tableName}, ({ one, many }) => ({\n${relationLines.join("\n")}\n}));`,
|
|
@@ -564,6 +990,10 @@ ${buildExternalTableImportLines(externalTables)}
|
|
|
564
990
|
${tableBlocks.join("\n\n")}
|
|
565
991
|
|
|
566
992
|
${relationBlocks.join("\n\n")}
|
|
993
|
+
|
|
994
|
+
${emitManyToManyRuntimeMetadata(definition)}
|
|
995
|
+
|
|
996
|
+
${emitRuntimeRelationMetadata(definition)}
|
|
567
997
|
`;
|
|
568
998
|
}
|
|
569
999
|
|
|
@@ -114,7 +114,7 @@ type RelationAggregateSelectionInput<TModel extends Record<string, unknown>> = {
|
|
|
114
114
|
_avg?: RelationAvgSelectionInput<TModel>;
|
|
115
115
|
};
|
|
116
116
|
|
|
117
|
-
type
|
|
117
|
+
type QueryNativeWithRelationInput<TEntry> =
|
|
118
118
|
| (Extract<TEntry, true> extends never ? never : true)
|
|
119
119
|
| (Extract<TEntry, Record<string, unknown>> extends infer TRelation
|
|
120
120
|
? TRelation extends Record<string, unknown>
|
|
@@ -122,24 +122,57 @@ type QueryWithRelationInput<TEntry> =
|
|
|
122
122
|
RelationAggregateSelectionInput<RelationModelFromConfig<TRelation>> & {
|
|
123
123
|
where?: WhereInput<RelationModelFromConfig<TRelation>>;
|
|
124
124
|
with?: TRelation extends { with?: infer TNestedWith }
|
|
125
|
-
?
|
|
125
|
+
? QueryWithInputForTable<TableName, TNestedWith>
|
|
126
126
|
: never;
|
|
127
127
|
}
|
|
128
128
|
: never
|
|
129
129
|
: never);
|
|
130
130
|
|
|
131
|
-
type
|
|
131
|
+
type QueryManyToManyWithRelationInput<TTargetTable extends TableName> =
|
|
132
|
+
| true
|
|
133
|
+
| (Omit<NonNullable<TableFindManyArgs<TTargetTable>>, "where" | "with"> &
|
|
134
|
+
RelationAggregateSelectionInput<TableModel<TTargetTable>> & {
|
|
135
|
+
where?: WhereInput<TableModel<TTargetTable>>;
|
|
136
|
+
with?: QueryWithInputForTable<
|
|
137
|
+
TTargetTable,
|
|
138
|
+
NativeFindManyWith<TTargetTable>
|
|
139
|
+
>;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
type QueryWithRelationInputForTable<
|
|
143
|
+
TSourceTable extends TableName,
|
|
144
|
+
TRelationName extends string,
|
|
145
|
+
TEntry,
|
|
146
|
+
> = ManyToManyTargetTableName<TSourceTable, TRelationName> extends infer TTarget
|
|
147
|
+
? TTarget extends TableName
|
|
148
|
+
? QueryManyToManyWithRelationInput<TTarget>
|
|
149
|
+
: QueryNativeWithRelationInput<TEntry>
|
|
150
|
+
: QueryNativeWithRelationInput<TEntry>;
|
|
151
|
+
|
|
152
|
+
type QueryWithInputForTable<
|
|
153
|
+
TSourceTable extends TableName,
|
|
154
|
+
TWith,
|
|
155
|
+
> = TWith extends Record<string, unknown>
|
|
132
156
|
? {
|
|
133
|
-
[K in keyof TWith]?:
|
|
157
|
+
[K in keyof TWith]?: QueryWithRelationInputForTable<
|
|
158
|
+
TSourceTable,
|
|
159
|
+
Extract<K, string>,
|
|
160
|
+
TWith[K]
|
|
161
|
+
>;
|
|
134
162
|
}
|
|
135
163
|
: TWith;
|
|
136
164
|
|
|
165
|
+
type QueryWithInput<TName extends TableName, TWith> = QueryWithInputForTable<
|
|
166
|
+
TName,
|
|
167
|
+
TWith
|
|
168
|
+
>;
|
|
169
|
+
|
|
137
170
|
export type QueryFindManyArgs<TName extends TableName> = Omit<
|
|
138
171
|
NonNullable<TableFindManyArgs<TName>>,
|
|
139
172
|
"where" | "with"
|
|
140
173
|
> & {
|
|
141
174
|
where?: WhereInput<TableModel<TName>>;
|
|
142
|
-
with?: QueryWithInput<NativeFindManyWith<TName>>;
|
|
175
|
+
with?: QueryWithInput<TName, NativeFindManyWith<TName>>;
|
|
143
176
|
};
|
|
144
177
|
|
|
145
178
|
export type QueryFindFirstArgs<TName extends TableName> = Omit<
|
|
@@ -147,11 +180,59 @@ export type QueryFindFirstArgs<TName extends TableName> = Omit<
|
|
|
147
180
|
"where" | "with"
|
|
148
181
|
> & {
|
|
149
182
|
where?: WhereInput<TableModel<TName>>;
|
|
150
|
-
with?: QueryWithInput<NativeFindFirstWith<TName>>;
|
|
183
|
+
with?: QueryWithInput<TName, NativeFindFirstWith<TName>>;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
type SingularizeRelationName<TName extends string> =
|
|
187
|
+
TName extends \`\${infer TRoot\}ies\`
|
|
188
|
+
? \`\${TRoot\}y\`
|
|
189
|
+
: TName extends \`\${infer TRoot\}s\`
|
|
190
|
+
? TRoot
|
|
191
|
+
: TName;
|
|
192
|
+
|
|
193
|
+
type RelationInsertItem<TTargetTable extends TableName> =
|
|
194
|
+
| TableModel<TTargetTable>
|
|
195
|
+
| TableInsertModel<TTargetTable>
|
|
196
|
+
| ("id" extends keyof TableModel<TTargetTable>
|
|
197
|
+
? TableModel<TTargetTable>["id"]
|
|
198
|
+
: never);
|
|
199
|
+
|
|
200
|
+
type RelationInsertValue<
|
|
201
|
+
TSourceTable extends TableName,
|
|
202
|
+
TRelationName extends RuntimeRelationName<TSourceTable>,
|
|
203
|
+
> = RuntimeRelationTargetTable<TSourceTable, TRelationName> extends infer TTargetTable
|
|
204
|
+
? TTargetTable extends TableName
|
|
205
|
+
? RuntimeRelationKind<TSourceTable, TRelationName> extends "one"
|
|
206
|
+
? RelationInsertItem<TTargetTable>
|
|
207
|
+
: RelationInsertItem<TTargetTable> | Array<RelationInsertItem<TTargetTable>>
|
|
208
|
+
: never
|
|
209
|
+
: never;
|
|
210
|
+
|
|
211
|
+
type RelationInsertFields<TName extends TableName> = {
|
|
212
|
+
[TRelationName in RuntimeRelationName<TName>]?: RelationInsertValue<
|
|
213
|
+
TName,
|
|
214
|
+
TRelationName
|
|
215
|
+
>;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
type RelationInsertIdAliasFields<TName extends TableName> = {
|
|
219
|
+
[TRelationName in RuntimeRelationName<TName> as \`\${TRelationName\}Id\`]?: RelationInsertValue<
|
|
220
|
+
TName,
|
|
221
|
+
TRelationName
|
|
222
|
+
>;
|
|
223
|
+
} & {
|
|
224
|
+
[TRelationName in RuntimeRelationName<TName> as \`\${SingularizeRelationName<TRelationName>\}Id\`]?: RelationInsertValue<
|
|
225
|
+
TName,
|
|
226
|
+
TRelationName
|
|
227
|
+
>;
|
|
151
228
|
};
|
|
152
229
|
|
|
230
|
+
type QueryInsertValue<TName extends TableName> = TableInsertScalarModel<TName> &
|
|
231
|
+
RelationInsertFields<TName> &
|
|
232
|
+
RelationInsertIdAliasFields<TName>;
|
|
233
|
+
|
|
153
234
|
export type QueryInsertArgs<TName extends TableName> = {
|
|
154
|
-
values:
|
|
235
|
+
values: QueryInsertValue<TName> | Array<QueryInsertValue<TName>>;
|
|
155
236
|
};
|
|
156
237
|
|
|
157
238
|
export type QueryUpdateArgs<TName extends TableName> = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export function generateQueryApiTypesSection(): string {
|
|
2
2
|
return `type AggregateWithInput<TName extends TableName> =
|
|
3
|
-
QueryWithInput<NativeFindManyWith<TName>>;
|
|
3
|
+
QueryWithInput<TName, NativeFindManyWith<TName>>;
|
|
4
4
|
|
|
5
5
|
type NumericFieldKey<TName extends TableName> = NumericModelFieldKey<
|
|
6
6
|
TableModel<TName>
|