@prisma-next/sql-contract-ts 0.12.0-dev.59 → 0.12.0-dev.60
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/dist/{build-contract-C9dZNMRg.mjs → build-contract-6Rvs461H.mjs} +36 -1
- package/dist/build-contract-6Rvs461H.mjs.map +1 -0
- package/dist/config-types.mjs +1 -1
- package/dist/contract-builder.d.mts +130 -10
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +185 -32
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +10 -10
- package/src/build-contract.ts +43 -0
- package/src/contract-builder.ts +2 -1
- package/src/contract-definition.ts +17 -0
- package/src/contract-dsl.ts +324 -17
- package/src/contract-lowering.ts +169 -15
- package/src/exports/contract-builder.ts +2 -0
- package/dist/build-contract-C9dZNMRg.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-contract-ts",
|
|
3
|
-
"version": "0.12.0-dev.
|
|
3
|
+
"version": "0.12.0-dev.60",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "SQL-specific TypeScript contract authoring surface for Prisma Next",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/config": "0.12.0-dev.
|
|
10
|
-
"@prisma-next/contract": "0.12.0-dev.
|
|
11
|
-
"@prisma-next/contract-authoring": "0.12.0-dev.
|
|
12
|
-
"@prisma-next/framework-components": "0.12.0-dev.
|
|
13
|
-
"@prisma-next/sql-contract": "0.12.0-dev.
|
|
14
|
-
"@prisma-next/utils": "0.12.0-dev.
|
|
9
|
+
"@prisma-next/config": "0.12.0-dev.60",
|
|
10
|
+
"@prisma-next/contract": "0.12.0-dev.60",
|
|
11
|
+
"@prisma-next/contract-authoring": "0.12.0-dev.60",
|
|
12
|
+
"@prisma-next/framework-components": "0.12.0-dev.60",
|
|
13
|
+
"@prisma-next/sql-contract": "0.12.0-dev.60",
|
|
14
|
+
"@prisma-next/utils": "0.12.0-dev.60",
|
|
15
15
|
"arktype": "^2.2.0",
|
|
16
16
|
"pathe": "^2.0.3",
|
|
17
17
|
"ts-toolbelt": "^9.6.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@prisma-next/test-utils": "0.12.0-dev.
|
|
21
|
-
"@prisma-next/tsconfig": "0.12.0-dev.
|
|
20
|
+
"@prisma-next/test-utils": "0.12.0-dev.60",
|
|
21
|
+
"@prisma-next/tsconfig": "0.12.0-dev.60",
|
|
22
22
|
"@types/pg": "8.20.0",
|
|
23
23
|
"pg": "8.21.0",
|
|
24
|
-
"@prisma-next/tsdown": "0.12.0-dev.
|
|
24
|
+
"@prisma-next/tsdown": "0.12.0-dev.60",
|
|
25
25
|
"tsdown": "0.22.1",
|
|
26
26
|
"typescript": "5.9.3",
|
|
27
27
|
"vitest": "4.1.7"
|
package/src/build-contract.ts
CHANGED
|
@@ -425,6 +425,31 @@ export function buildSqlContractFromDefinition(
|
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
const foreignKeys = (semanticModel.foreignKeys ?? []).map((fk) => {
|
|
428
|
+
if (fk.references.spaceId !== undefined) {
|
|
429
|
+
// Cross-space FK: the target lives in a different contract space.
|
|
430
|
+
// Skip local model lookup and carry the spaceId coordinate through.
|
|
431
|
+
const targetNamespaceId = fk.references.namespaceId ?? defaultNamespaceId;
|
|
432
|
+
return {
|
|
433
|
+
source: { namespaceId: asNamespaceId(namespaceId), tableName, columns: fk.columns },
|
|
434
|
+
target: {
|
|
435
|
+
namespaceId: asNamespaceId(targetNamespaceId),
|
|
436
|
+
tableName: fk.references.table,
|
|
437
|
+
columns: fk.references.columns,
|
|
438
|
+
spaceId: fk.references.spaceId,
|
|
439
|
+
},
|
|
440
|
+
...applyFkDefaults(
|
|
441
|
+
{
|
|
442
|
+
...ifDefined('constraint', fk.constraint),
|
|
443
|
+
...ifDefined('index', fk.index),
|
|
444
|
+
},
|
|
445
|
+
definition.foreignKeyDefaults,
|
|
446
|
+
),
|
|
447
|
+
...ifDefined('name', fk.name),
|
|
448
|
+
...ifDefined('onDelete', fk.onDelete),
|
|
449
|
+
...ifDefined('onUpdate', fk.onUpdate),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
428
453
|
const targetModel = assertKnownTargetModel(
|
|
429
454
|
modelsByName,
|
|
430
455
|
semanticModel.modelName,
|
|
@@ -523,6 +548,24 @@ export function buildSqlContractFromDefinition(
|
|
|
523
548
|
);
|
|
524
549
|
const modelRelations: Record<string, ContractRelation> = {};
|
|
525
550
|
for (const relation of semanticModel.relations ?? []) {
|
|
551
|
+
// Cross-space relations have `spaceId` set — the target model lives in
|
|
552
|
+
// a different contract space, so skip local model lookup and validation.
|
|
553
|
+
if (relation.spaceId !== undefined) {
|
|
554
|
+
const targetNamespaceId = relation.namespaceId ?? defaultNamespaceId;
|
|
555
|
+
modelRelations[relation.fieldName] = {
|
|
556
|
+
to: crossRef(relation.toModel, targetNamespaceId, relation.spaceId),
|
|
557
|
+
// Cross-space belongsTo relations are always N:1 (the FK-owning side).
|
|
558
|
+
cardinality: 'N:1',
|
|
559
|
+
on: {
|
|
560
|
+
localFields: relation.on.parentColumns.map((col) => columnToField.get(col) ?? col),
|
|
561
|
+
// For cross-space targets the lowering carries field names directly
|
|
562
|
+
// (no fieldToColumn map available for the remote model).
|
|
563
|
+
targetFields: relation.on.childColumns,
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
|
|
526
569
|
const targetModel = assertKnownTargetModel(
|
|
527
570
|
modelsByName,
|
|
528
571
|
semanticModel.modelName,
|
package/src/contract-builder.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
type ContractInput,
|
|
24
24
|
type ContractModelBuilder,
|
|
25
|
+
extensionModel,
|
|
25
26
|
field,
|
|
26
27
|
isContractInput,
|
|
27
28
|
type ModelAttributesSpec,
|
|
@@ -529,4 +530,4 @@ export type {
|
|
|
529
530
|
ModelLike,
|
|
530
531
|
ScalarFieldBuilder,
|
|
531
532
|
};
|
|
532
|
-
export { field, model, rel };
|
|
533
|
+
export { extensionModel, field, model, rel };
|
|
@@ -59,6 +59,12 @@ export interface ForeignKeyNode {
|
|
|
59
59
|
* know the target namespace can stamp it explicitly.
|
|
60
60
|
*/
|
|
61
61
|
readonly namespaceId?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Contract-space identity of the referenced table. When present, the
|
|
64
|
+
* table lives in a different contract space (identified by this value)
|
|
65
|
+
* rather than the current contract. Absent for local FKs.
|
|
66
|
+
*/
|
|
67
|
+
readonly spaceId?: string;
|
|
62
68
|
};
|
|
63
69
|
readonly name?: string;
|
|
64
70
|
readonly onDelete?: ReferentialAction;
|
|
@@ -72,6 +78,17 @@ export interface RelationNode {
|
|
|
72
78
|
readonly toModel: string;
|
|
73
79
|
readonly toTable: string;
|
|
74
80
|
readonly cardinality: '1:1' | '1:N' | 'N:1' | 'N:M';
|
|
81
|
+
/**
|
|
82
|
+
* Contract-space identity of the related model. When present, the
|
|
83
|
+
* related model lives in a different contract space. Absent for local
|
|
84
|
+
* (same-space) relations.
|
|
85
|
+
*/
|
|
86
|
+
readonly spaceId?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Namespace coordinate of the related model in the foreign space.
|
|
89
|
+
* Only set when `spaceId` is present.
|
|
90
|
+
*/
|
|
91
|
+
readonly namespaceId?: string;
|
|
75
92
|
readonly on: {
|
|
76
93
|
readonly parentTable: string;
|
|
77
94
|
readonly parentColumns: readonly string[];
|
package/src/contract-dsl.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
SqlNamespaceTablesInput,
|
|
22
22
|
StorageTypeInstance,
|
|
23
23
|
} from '@prisma-next/sql-contract/types';
|
|
24
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
24
25
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
25
26
|
import type { NamedConstraintSpec } from './authoring-type-utils';
|
|
26
27
|
import type { EnumTypeHandle } from './enum-type';
|
|
@@ -157,6 +158,16 @@ export class ScalarFieldBuilder<State extends AnyScalarFieldState = AnyScalarFie
|
|
|
157
158
|
|
|
158
159
|
constructor(private readonly state: State) {}
|
|
159
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Returns the physical column name when `.column(name)` was called, or
|
|
163
|
+
* `undefined` when the field uses the default (logical field name) mapping.
|
|
164
|
+
* Used by cross-space FK lowering to stamp the physical column name onto
|
|
165
|
+
* `TargetFieldRef.columnName` so FK target columns are resolved correctly.
|
|
166
|
+
*/
|
|
167
|
+
get physicalColumnName(): string | undefined {
|
|
168
|
+
return this.state.columnName;
|
|
169
|
+
}
|
|
170
|
+
|
|
160
171
|
optional(): ScalarFieldBuilder<
|
|
161
172
|
State extends ScalarFieldState<
|
|
162
173
|
infer CodecId,
|
|
@@ -442,6 +453,22 @@ type BelongsToRelation<
|
|
|
442
453
|
readonly from: FromField;
|
|
443
454
|
readonly to: ToField;
|
|
444
455
|
readonly sql?: SqlSpec;
|
|
456
|
+
/**
|
|
457
|
+
* Contract-space identity of the target model. Populated when
|
|
458
|
+
* `belongsTo` receives a cross-space branded handle. Absent for
|
|
459
|
+
* local (same-space) relations.
|
|
460
|
+
*/
|
|
461
|
+
readonly spaceId?: string;
|
|
462
|
+
/**
|
|
463
|
+
* Physical table name of the cross-space target model. Only set
|
|
464
|
+
* when `spaceId` is present; read from the handle's `tableName`.
|
|
465
|
+
*/
|
|
466
|
+
readonly tableName?: string;
|
|
467
|
+
/**
|
|
468
|
+
* Namespace coordinate of the cross-space target model.
|
|
469
|
+
* Only set when `spaceId` is present.
|
|
470
|
+
*/
|
|
471
|
+
readonly namespaceId?: string;
|
|
445
472
|
};
|
|
446
473
|
|
|
447
474
|
type HasManyRelation<
|
|
@@ -533,27 +560,69 @@ export class RelationBuilder<State extends RelationState = AnyRelationState> {
|
|
|
533
560
|
}
|
|
534
561
|
}
|
|
535
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Reference to a column on the current (local) model.
|
|
565
|
+
*
|
|
566
|
+
* Source columns are always local to the contract being authored. The
|
|
567
|
+
* cross-space brand lives on `TargetFieldRef` (the target side of a foreign
|
|
568
|
+
* key), not here.
|
|
569
|
+
*/
|
|
536
570
|
export type ColumnRef<FieldName extends string = string> = {
|
|
537
571
|
readonly kind: 'columnRef';
|
|
538
572
|
readonly fieldName: FieldName;
|
|
539
573
|
};
|
|
540
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Reference to a field on a target model, produced by model `.refs` and
|
|
577
|
+
* `constraints.ref(modelName, fieldName)`.
|
|
578
|
+
*
|
|
579
|
+
* The `TSpaceId` phantom parameter carries the contract-space identity of the
|
|
580
|
+
* target model. Local model handles produce `TSpaceId = '<self>'`; extension
|
|
581
|
+
* handles carry the extension's `spaceId`. The brand is propagated from the
|
|
582
|
+
* parent `ContractModelBuilder` via the `spaceId?` property: absent means local
|
|
583
|
+
* (`'<self>'`), present means cross-space.
|
|
584
|
+
*/
|
|
541
585
|
export type TargetFieldRef<
|
|
542
586
|
ModelName extends string = string,
|
|
543
587
|
FieldName extends string = string,
|
|
544
|
-
|
|
588
|
+
TSpaceId extends string = string,
|
|
545
589
|
> = {
|
|
546
590
|
readonly kind: 'targetFieldRef';
|
|
547
|
-
readonly source:
|
|
591
|
+
readonly source: TargetFieldRefSource;
|
|
548
592
|
readonly modelName: ModelName;
|
|
549
593
|
readonly fieldName: FieldName;
|
|
594
|
+
/**
|
|
595
|
+
* Cross-space discriminator. When present, the referenced model lives in a
|
|
596
|
+
* different contract space identified by this value. Absent for local refs.
|
|
597
|
+
*/
|
|
598
|
+
readonly spaceId?: TSpaceId extends '<self>' ? never : TSpaceId;
|
|
599
|
+
/**
|
|
600
|
+
* Namespace id of the cross-space target model (e.g. `'auth'` for
|
|
601
|
+
* `supabase` `auth.User`). Only present for cross-space refs.
|
|
602
|
+
*/
|
|
603
|
+
readonly namespaceId?: string;
|
|
604
|
+
/**
|
|
605
|
+
* Physical table name of the cross-space target model. Only present for
|
|
606
|
+
* cross-space refs; allows the lowering path to bypass the local model
|
|
607
|
+
* registry.
|
|
608
|
+
*/
|
|
609
|
+
readonly tableName?: string;
|
|
610
|
+
/**
|
|
611
|
+
* Physical column name of the target field. Populated for cross-space refs
|
|
612
|
+
* when the extension handle's field used `.column(name)` to rename the
|
|
613
|
+
* physical column. When absent the logical `fieldName` is used as the column
|
|
614
|
+
* name. Only relevant for cross-space FK lowering — local FKs resolve column
|
|
615
|
+
* names via the local `fieldToColumn` map.
|
|
616
|
+
*/
|
|
617
|
+
readonly columnName?: string;
|
|
550
618
|
};
|
|
551
619
|
|
|
552
620
|
export type ModelTokenRefs<
|
|
553
621
|
ModelName extends string,
|
|
554
622
|
Fields extends Record<string, ScalarFieldBuilder>,
|
|
623
|
+
TSpaceId extends string = '<self>',
|
|
555
624
|
> = {
|
|
556
|
-
readonly [K in keyof Fields]: TargetFieldRef<ModelName, K & string>;
|
|
625
|
+
readonly [K in keyof Fields]: TargetFieldRef<ModelName, K & string, TSpaceId>;
|
|
557
626
|
};
|
|
558
627
|
|
|
559
628
|
type ConstraintOptions<Name extends string | undefined = string | undefined> = {
|
|
@@ -625,6 +694,22 @@ export type ForeignKeyConstraint<
|
|
|
625
694
|
readonly targetModel: TargetModelName;
|
|
626
695
|
readonly targetFields: TargetFieldNames;
|
|
627
696
|
readonly targetSource?: TargetFieldRefSource;
|
|
697
|
+
/**
|
|
698
|
+
* Cross-space discriminator. When present, the FK target lives in a
|
|
699
|
+
* different contract space identified by this value. Absent for local FKs.
|
|
700
|
+
*/
|
|
701
|
+
readonly targetSpaceId?: string;
|
|
702
|
+
/**
|
|
703
|
+
* Namespace coordinate of the cross-space target model. Populated when
|
|
704
|
+
* the target model handle carries a `namespace` (e.g. `auth` for supabase
|
|
705
|
+
* `auth.User`). Absent for local FKs.
|
|
706
|
+
*/
|
|
707
|
+
readonly targetNamespaceId?: string;
|
|
708
|
+
/**
|
|
709
|
+
* Table name of the cross-space target. Populated for cross-space FKs
|
|
710
|
+
* so the lowering path doesn't need a local model lookup.
|
|
711
|
+
*/
|
|
712
|
+
readonly targetTableName?: string;
|
|
628
713
|
readonly name?: Name;
|
|
629
714
|
readonly onDelete?: 'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault';
|
|
630
715
|
readonly onUpdate?: 'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault';
|
|
@@ -640,6 +725,9 @@ function normalizeTargetFieldRefInput(input: TargetFieldRef | readonly TargetFie
|
|
|
640
725
|
readonly modelName: string;
|
|
641
726
|
readonly fieldNames: readonly string[];
|
|
642
727
|
readonly source: TargetFieldRefSource;
|
|
728
|
+
readonly spaceId: string | undefined;
|
|
729
|
+
readonly namespaceId: string | undefined;
|
|
730
|
+
readonly tableName: string | undefined;
|
|
643
731
|
} {
|
|
644
732
|
const refs = Array.isArray(input) ? input : [input];
|
|
645
733
|
const [first] = refs;
|
|
@@ -649,10 +737,33 @@ function normalizeTargetFieldRefInput(input: TargetFieldRef | readonly TargetFie
|
|
|
649
737
|
if (refs.some((ref) => ref.modelName !== first.modelName)) {
|
|
650
738
|
throw new Error('All target refs in a foreign key must point to the same model');
|
|
651
739
|
}
|
|
740
|
+
// F-compound: all refs in a compound FK must share the same cross-space coordinate.
|
|
741
|
+
// A mismatch in spaceId, namespaceId, or tableName means the refs come from
|
|
742
|
+
// different spaces despite having the same modelName — an impossible FK.
|
|
743
|
+
if (refs.some((ref) => ref.spaceId !== first.spaceId)) {
|
|
744
|
+
throw new Error(
|
|
745
|
+
`All target refs in a compound foreign key must share the same spaceId (found mismatch: "${first.spaceId ?? '<local>'}" vs "${refs.find((r) => r.spaceId !== first.spaceId)?.spaceId ?? '<local>'}")`,
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
if (refs.some((ref) => ref.namespaceId !== first.namespaceId)) {
|
|
749
|
+
throw new Error(
|
|
750
|
+
'All target refs in a compound foreign key must share the same namespaceId (found mismatch)',
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
if (refs.some((ref) => ref.tableName !== first.tableName)) {
|
|
754
|
+
throw new Error(
|
|
755
|
+
'All target refs in a compound foreign key must share the same tableName (found mismatch)',
|
|
756
|
+
);
|
|
757
|
+
}
|
|
652
758
|
return {
|
|
653
759
|
modelName: first.modelName,
|
|
654
|
-
|
|
760
|
+
// F-col: for cross-space refs, prefer the physical column name (columnName) over
|
|
761
|
+
// the logical field name. Local refs have no columnName and use fieldName directly.
|
|
762
|
+
fieldNames: refs.map((ref) => ref.columnName ?? ref.fieldName),
|
|
655
763
|
source: refs.some((ref) => ref.source === 'string') ? 'string' : 'token',
|
|
764
|
+
spaceId: first.spaceId,
|
|
765
|
+
namespaceId: first.namespaceId,
|
|
766
|
+
tableName: first.tableName,
|
|
656
767
|
};
|
|
657
768
|
}
|
|
658
769
|
|
|
@@ -772,6 +883,15 @@ function createConstraintsDsl<IndexTypes extends IndexTypeMap = Record<never, ne
|
|
|
772
883
|
targetModel: normalizedTarget.modelName,
|
|
773
884
|
targetFields: normalizedTarget.fieldNames,
|
|
774
885
|
targetSource: normalizedTarget.source,
|
|
886
|
+
...(normalizedTarget.spaceId !== undefined
|
|
887
|
+
? { targetSpaceId: normalizedTarget.spaceId }
|
|
888
|
+
: {}),
|
|
889
|
+
...(normalizedTarget.namespaceId !== undefined
|
|
890
|
+
? { targetNamespaceId: normalizedTarget.namespaceId }
|
|
891
|
+
: {}),
|
|
892
|
+
...(normalizedTarget.tableName !== undefined
|
|
893
|
+
? { targetTableName: normalizedTarget.tableName }
|
|
894
|
+
: {}),
|
|
775
895
|
...(options?.name ? { name: options.name } : {}),
|
|
776
896
|
...(options?.onDelete ? { onDelete: options.onDelete } : {}),
|
|
777
897
|
...(options?.onUpdate ? { onUpdate: options.onUpdate } : {}),
|
|
@@ -847,17 +967,40 @@ function createFieldRefs<Fields extends Record<string, ScalarFieldBuilder>>(
|
|
|
847
967
|
function createModelTokenRefs<
|
|
848
968
|
ModelName extends string,
|
|
849
969
|
Fields extends Record<string, ScalarFieldBuilder>,
|
|
850
|
-
|
|
970
|
+
TSpaceId extends string = '<self>',
|
|
971
|
+
>(
|
|
972
|
+
modelName: ModelName,
|
|
973
|
+
fields: Fields,
|
|
974
|
+
crossSpaceCoordinate?: {
|
|
975
|
+
readonly spaceId: TSpaceId;
|
|
976
|
+
readonly namespaceId?: string;
|
|
977
|
+
readonly tableName?: string;
|
|
978
|
+
},
|
|
979
|
+
): ModelTokenRefs<ModelName, Fields, TSpaceId> {
|
|
851
980
|
const refs = {} as Record<string, TargetFieldRef>;
|
|
852
|
-
for (const fieldName of Object.
|
|
981
|
+
for (const [fieldName, fieldBuilder] of Object.entries(fields)) {
|
|
982
|
+
const physicalColumn =
|
|
983
|
+
crossSpaceCoordinate !== undefined ? fieldBuilder.physicalColumnName : undefined;
|
|
853
984
|
refs[fieldName] = {
|
|
854
985
|
kind: 'targetFieldRef',
|
|
855
986
|
source: 'token',
|
|
856
987
|
modelName,
|
|
857
988
|
fieldName,
|
|
989
|
+
...(crossSpaceCoordinate !== undefined
|
|
990
|
+
? {
|
|
991
|
+
spaceId: crossSpaceCoordinate.spaceId,
|
|
992
|
+
...(crossSpaceCoordinate.namespaceId !== undefined
|
|
993
|
+
? { namespaceId: crossSpaceCoordinate.namespaceId }
|
|
994
|
+
: {}),
|
|
995
|
+
...(crossSpaceCoordinate.tableName !== undefined
|
|
996
|
+
? { tableName: crossSpaceCoordinate.tableName }
|
|
997
|
+
: {}),
|
|
998
|
+
...(physicalColumn !== undefined ? { columnName: physicalColumn } : {}),
|
|
999
|
+
}
|
|
1000
|
+
: {}),
|
|
858
1001
|
};
|
|
859
1002
|
}
|
|
860
|
-
return refs as ModelTokenRefs<ModelName, Fields>;
|
|
1003
|
+
return refs as ModelTokenRefs<ModelName, Fields, TSpaceId>;
|
|
861
1004
|
}
|
|
862
1005
|
|
|
863
1006
|
type StageInput<Context, Spec> = Spec | ((context: Context) => Spec);
|
|
@@ -1002,6 +1145,7 @@ export class ContractModelBuilder<
|
|
|
1002
1145
|
AttributesSpec extends ModelAttributesSpec | undefined = undefined,
|
|
1003
1146
|
SqlSpec extends SqlStageSpec | undefined = undefined,
|
|
1004
1147
|
IndexTypes extends IndexTypeMap = Record<never, never>,
|
|
1148
|
+
TSpaceId extends string = '<self>',
|
|
1005
1149
|
> {
|
|
1006
1150
|
declare readonly __name: ModelName;
|
|
1007
1151
|
declare readonly __fields: Fields;
|
|
@@ -1009,7 +1153,8 @@ export class ContractModelBuilder<
|
|
|
1009
1153
|
declare readonly __attributes: AttributesSpec;
|
|
1010
1154
|
declare readonly __sql: SqlSpec;
|
|
1011
1155
|
declare readonly __indexTypes: IndexTypes;
|
|
1012
|
-
readonly
|
|
1156
|
+
declare readonly __spaceId: TSpaceId;
|
|
1157
|
+
readonly refs: ModelName extends string ? ModelTokenRefs<ModelName, Fields, TSpaceId> : never;
|
|
1013
1158
|
|
|
1014
1159
|
constructor(
|
|
1015
1160
|
readonly stageOne: {
|
|
@@ -1020,10 +1165,25 @@ export class ContractModelBuilder<
|
|
|
1020
1165
|
},
|
|
1021
1166
|
readonly attributesFactory?: StageInput<AttributeContext<Fields>, AttributesSpec>,
|
|
1022
1167
|
readonly sqlFactory?: StageInput<SqlContext<Fields, IndexTypes>, SqlSpec>,
|
|
1168
|
+
readonly spaceId?: TSpaceId,
|
|
1169
|
+
readonly tableName?: string,
|
|
1023
1170
|
) {
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1171
|
+
const crossSpaceCoordinate =
|
|
1172
|
+
spaceId !== undefined
|
|
1173
|
+
? {
|
|
1174
|
+
spaceId,
|
|
1175
|
+
...(stageOne.namespace !== undefined ? { namespaceId: stageOne.namespace } : {}),
|
|
1176
|
+
...(tableName !== undefined ? { tableName } : {}),
|
|
1177
|
+
}
|
|
1178
|
+
: undefined;
|
|
1179
|
+
this.refs = blindCast<
|
|
1180
|
+
ModelName extends string ? ModelTokenRefs<ModelName, Fields, TSpaceId> : never,
|
|
1181
|
+
'conditional generic: stageOne.modelName presence matches ModelName extends string'
|
|
1182
|
+
>(
|
|
1183
|
+
stageOne.modelName
|
|
1184
|
+
? createModelTokenRefs(stageOne.modelName, stageOne.fields, crossSpaceCoordinate)
|
|
1185
|
+
: undefined,
|
|
1186
|
+
);
|
|
1027
1187
|
}
|
|
1028
1188
|
|
|
1029
1189
|
ref<FieldName extends keyof Fields & string>(
|
|
@@ -1053,7 +1213,8 @@ export class ContractModelBuilder<
|
|
|
1053
1213
|
Relations & NextRelations,
|
|
1054
1214
|
AttributesSpec,
|
|
1055
1215
|
SqlSpec,
|
|
1056
|
-
IndexTypes
|
|
1216
|
+
IndexTypes,
|
|
1217
|
+
TSpaceId
|
|
1057
1218
|
> {
|
|
1058
1219
|
const duplicateRelationName = findDuplicateRelationName(this.stageOne.relations, relations);
|
|
1059
1220
|
if (duplicateRelationName) {
|
|
@@ -1072,6 +1233,8 @@ export class ContractModelBuilder<
|
|
|
1072
1233
|
},
|
|
1073
1234
|
this.attributesFactory,
|
|
1074
1235
|
this.sqlFactory,
|
|
1236
|
+
this.spaceId,
|
|
1237
|
+
this.tableName,
|
|
1075
1238
|
);
|
|
1076
1239
|
}
|
|
1077
1240
|
|
|
@@ -1080,17 +1243,61 @@ export class ContractModelBuilder<
|
|
|
1080
1243
|
AttributeContext<Fields>,
|
|
1081
1244
|
ValidateAttributesStageSpec<Fields, SqlSpec, NextAttributesSpec>
|
|
1082
1245
|
>,
|
|
1083
|
-
): ContractModelBuilder<
|
|
1084
|
-
|
|
1246
|
+
): ContractModelBuilder<
|
|
1247
|
+
ModelName,
|
|
1248
|
+
Fields,
|
|
1249
|
+
Relations,
|
|
1250
|
+
NextAttributesSpec,
|
|
1251
|
+
SqlSpec,
|
|
1252
|
+
IndexTypes,
|
|
1253
|
+
TSpaceId
|
|
1254
|
+
> {
|
|
1255
|
+
return new ContractModelBuilder(
|
|
1256
|
+
this.stageOne,
|
|
1257
|
+
specOrFactory,
|
|
1258
|
+
this.sqlFactory,
|
|
1259
|
+
this.spaceId,
|
|
1260
|
+
this.tableName,
|
|
1261
|
+
);
|
|
1085
1262
|
}
|
|
1086
1263
|
|
|
1087
1264
|
sql<const NextSqlSpec extends SqlStageSpec>(
|
|
1088
1265
|
specOrFactory: StageInput<SqlContext<Fields, IndexTypes>, NextSqlSpec>,
|
|
1089
1266
|
): [ValidateSqlStageSpec<Fields, AttributesSpec, NextSqlSpec>] extends [never]
|
|
1090
|
-
? ContractModelBuilder<
|
|
1091
|
-
|
|
1267
|
+
? ContractModelBuilder<
|
|
1268
|
+
ModelName,
|
|
1269
|
+
Fields,
|
|
1270
|
+
Relations,
|
|
1271
|
+
AttributesSpec,
|
|
1272
|
+
never,
|
|
1273
|
+
IndexTypes,
|
|
1274
|
+
TSpaceId
|
|
1275
|
+
>
|
|
1276
|
+
: ContractModelBuilder<
|
|
1277
|
+
ModelName,
|
|
1278
|
+
Fields,
|
|
1279
|
+
Relations,
|
|
1280
|
+
AttributesSpec,
|
|
1281
|
+
NextSqlSpec,
|
|
1282
|
+
IndexTypes,
|
|
1283
|
+
TSpaceId
|
|
1284
|
+
> {
|
|
1092
1285
|
// Conditional return type cannot be verified by the implementation; the runtime value is always a valid ContractModelBuilder regardless of the validation outcome (validation is type-level only).
|
|
1093
|
-
|
|
1286
|
+
// When specOrFactory is a static object (not a function), extract tableName for the cross-space coordinate.
|
|
1287
|
+
const nextTableName =
|
|
1288
|
+
typeof specOrFactory !== 'function' ? specOrFactory.table : this.tableName;
|
|
1289
|
+
return blindCast<
|
|
1290
|
+
never,
|
|
1291
|
+
'conditional return type; runtime value is always a valid ContractModelBuilder'
|
|
1292
|
+
>(
|
|
1293
|
+
new ContractModelBuilder(
|
|
1294
|
+
this.stageOne,
|
|
1295
|
+
this.attributesFactory,
|
|
1296
|
+
specOrFactory,
|
|
1297
|
+
this.spaceId,
|
|
1298
|
+
nextTableName,
|
|
1299
|
+
),
|
|
1300
|
+
);
|
|
1094
1301
|
}
|
|
1095
1302
|
|
|
1096
1303
|
buildAttributesSpec(): AttributesSpec {
|
|
@@ -1341,6 +1548,86 @@ export function model<
|
|
|
1341
1548
|
});
|
|
1342
1549
|
}
|
|
1343
1550
|
|
|
1551
|
+
/**
|
|
1552
|
+
* Factory for building a standalone branded extension model handle.
|
|
1553
|
+
*
|
|
1554
|
+
* Use this instead of `new ContractModelBuilder(…)` when constructing handles
|
|
1555
|
+
* for models that live in a foreign contract space (e.g. a Supabase extension
|
|
1556
|
+
* model referenced by a user's contract). The `spaceId` brands the returned
|
|
1557
|
+
* handle so `refs.<field>.spaceId` carries the foreign space identifier.
|
|
1558
|
+
*
|
|
1559
|
+
* @param name - The domain model name as declared in the foreign contract
|
|
1560
|
+
* (e.g. `'AuthUser'`, not a bare table alias like `'User'`).
|
|
1561
|
+
* @param input.namespace - The namespace within the foreign space (e.g. `'auth'`).
|
|
1562
|
+
* @param input.fields - Field definitions (use `field.column(…)`).
|
|
1563
|
+
* @param input.table - The physical table name in the foreign schema.
|
|
1564
|
+
* @param spaceId - The extension space identifier (e.g. `'supabase'`).
|
|
1565
|
+
*/
|
|
1566
|
+
export function extensionModel<
|
|
1567
|
+
const ModelName extends string,
|
|
1568
|
+
Fields extends Record<string, ScalarFieldBuilder>,
|
|
1569
|
+
const TSpaceId extends string,
|
|
1570
|
+
>(
|
|
1571
|
+
name: ModelName,
|
|
1572
|
+
input: {
|
|
1573
|
+
readonly namespace: string;
|
|
1574
|
+
readonly fields: Fields;
|
|
1575
|
+
readonly table: string;
|
|
1576
|
+
},
|
|
1577
|
+
spaceId: TSpaceId,
|
|
1578
|
+
): ContractModelBuilder<
|
|
1579
|
+
ModelName,
|
|
1580
|
+
Fields,
|
|
1581
|
+
Record<never, never>,
|
|
1582
|
+
undefined,
|
|
1583
|
+
undefined,
|
|
1584
|
+
Record<never, never>,
|
|
1585
|
+
TSpaceId
|
|
1586
|
+
> {
|
|
1587
|
+
const builder = new ContractModelBuilder<
|
|
1588
|
+
ModelName,
|
|
1589
|
+
Fields,
|
|
1590
|
+
Record<never, never>,
|
|
1591
|
+
undefined,
|
|
1592
|
+
undefined,
|
|
1593
|
+
Record<never, never>,
|
|
1594
|
+
TSpaceId
|
|
1595
|
+
>(
|
|
1596
|
+
{ modelName: name, namespace: input.namespace, fields: input.fields, relations: {} },
|
|
1597
|
+
undefined,
|
|
1598
|
+
undefined,
|
|
1599
|
+
spaceId,
|
|
1600
|
+
input.table,
|
|
1601
|
+
);
|
|
1602
|
+
return builder;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
/**
|
|
1606
|
+
* Narrow shape for detecting a cross-space branded model handle at runtime.
|
|
1607
|
+
* `ContractModelBuilder` exposes these fields but `AnyNamedModelToken` does
|
|
1608
|
+
* not declare them; this guard bridges the gap without a bare cast.
|
|
1609
|
+
*/
|
|
1610
|
+
type CrossSpaceHandle = {
|
|
1611
|
+
readonly spaceId: string;
|
|
1612
|
+
readonly tableName?: string;
|
|
1613
|
+
readonly stageOne: { readonly namespace?: string };
|
|
1614
|
+
};
|
|
1615
|
+
|
|
1616
|
+
function isCrossSpaceHandle(value: unknown): value is CrossSpaceHandle {
|
|
1617
|
+
if (typeof value !== 'object' || value === null) {
|
|
1618
|
+
return false;
|
|
1619
|
+
}
|
|
1620
|
+
const rec = blindCast<
|
|
1621
|
+
Record<PropertyKey, unknown>,
|
|
1622
|
+
'object null-check above; property access needed for runtime shape detection'
|
|
1623
|
+
>(value);
|
|
1624
|
+
return (
|
|
1625
|
+
typeof rec['spaceId'] === 'string' &&
|
|
1626
|
+
typeof rec['stageOne'] === 'object' &&
|
|
1627
|
+
rec['stageOne'] !== null
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1344
1631
|
function belongsTo<
|
|
1345
1632
|
Token extends AnyNamedModelToken,
|
|
1346
1633
|
FromField extends string | readonly string[],
|
|
@@ -1364,11 +1651,31 @@ function belongsTo(
|
|
|
1364
1651
|
readonly to: string | readonly string[];
|
|
1365
1652
|
},
|
|
1366
1653
|
): RelationBuilder<BelongsToRelation> {
|
|
1654
|
+
// F-lazy: when the model is a lazy thunk (() => handle), resolve it before
|
|
1655
|
+
// the brand check so cross-space handles passed as lazy tokens are detected.
|
|
1656
|
+
// normalizeRelationModelSource still receives the original toModel (which
|
|
1657
|
+
// handles both lazy and non-lazy forms correctly for model-name resolution).
|
|
1658
|
+
const resolvedModel = typeof toModel === 'function' ? toModel() : toModel;
|
|
1659
|
+
|
|
1660
|
+
// Extract cross-space brand from the handle when it carries a spaceId.
|
|
1661
|
+
// ContractModelBuilder exposes spaceId/tableName at runtime even though
|
|
1662
|
+
// the AnyNamedModelToken interface does not declare them.
|
|
1663
|
+
const crossSpaceCoordinate = isCrossSpaceHandle(resolvedModel)
|
|
1664
|
+
? {
|
|
1665
|
+
spaceId: resolvedModel.spaceId,
|
|
1666
|
+
...(resolvedModel.tableName !== undefined ? { tableName: resolvedModel.tableName } : {}),
|
|
1667
|
+
...(resolvedModel.stageOne.namespace !== undefined
|
|
1668
|
+
? { namespaceId: resolvedModel.stageOne.namespace }
|
|
1669
|
+
: {}),
|
|
1670
|
+
}
|
|
1671
|
+
: undefined;
|
|
1672
|
+
|
|
1367
1673
|
return new RelationBuilder({
|
|
1368
1674
|
kind: 'belongsTo',
|
|
1369
1675
|
toModel: normalizeRelationModelSource(toModel),
|
|
1370
1676
|
from: options.from,
|
|
1371
1677
|
to: options.to,
|
|
1678
|
+
...(crossSpaceCoordinate !== undefined ? crossSpaceCoordinate : {}),
|
|
1372
1679
|
});
|
|
1373
1680
|
}
|
|
1374
1681
|
|