@prisma-next/sql-contract-ts 0.12.0-dev.9 → 0.13.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/{build-contract-BCYW3_wE.mjs → build-contract-C-x2pfu4.mjs} +214 -76
- package/dist/build-contract-C-x2pfu4.mjs.map +1 -0
- package/dist/config-types.d.mts +7 -3
- package/dist/config-types.d.mts.map +1 -1
- package/dist/config-types.mjs +12 -9
- package/dist/config-types.mjs.map +1 -1
- package/dist/contract-builder.d.mts +319 -23
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +280 -44
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +13 -13
- package/schemas/data-contract-sql-v1.json +31 -0
- package/src/build-contract.ts +340 -95
- package/src/config-types.ts +24 -6
- package/src/contract-builder.ts +137 -16
- package/src/contract-definition.ts +57 -3
- package/src/contract-dsl.ts +346 -18
- package/src/contract-lowering.ts +188 -15
- package/src/contract-types.ts +18 -21
- package/src/enum-type.ts +236 -0
- package/src/exports/contract-builder.ts +5 -0
- package/dist/build-contract-BCYW3_wE.mjs.map +0 -1
package/src/contract-dsl.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ColumnDefault,
|
|
3
3
|
ColumnDefaultLiteralInputValue,
|
|
4
|
+
ControlPolicy,
|
|
4
5
|
ExecutionMutationDefaultPhases,
|
|
5
6
|
ExecutionMutationDefaultValue,
|
|
6
7
|
} from '@prisma-next/contract/types';
|
|
@@ -20,8 +21,11 @@ import type {
|
|
|
20
21
|
SqlNamespaceTablesInput,
|
|
21
22
|
StorageTypeInstance,
|
|
22
23
|
} from '@prisma-next/sql-contract/types';
|
|
24
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
23
25
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
24
26
|
import type { NamedConstraintSpec } from './authoring-type-utils';
|
|
27
|
+
import type { EnumTypeHandle } from './enum-type';
|
|
28
|
+
import { isEnumTypeHandle } from './enum-type';
|
|
25
29
|
|
|
26
30
|
export type NamingStrategy = 'identity' | 'snake_case';
|
|
27
31
|
|
|
@@ -30,7 +34,7 @@ export type NamingConfig = {
|
|
|
30
34
|
readonly columns?: NamingStrategy;
|
|
31
35
|
};
|
|
32
36
|
|
|
33
|
-
type NamedStorageTypeRef = string | StorageTypeInstance | PostgresEnumStorageEntry;
|
|
37
|
+
type NamedStorageTypeRef = string | StorageTypeInstance | PostgresEnumStorageEntry | EnumTypeHandle;
|
|
34
38
|
|
|
35
39
|
type NamedConstraintNameSpec<Name extends string = string> = {
|
|
36
40
|
readonly name: Name;
|
|
@@ -154,6 +158,16 @@ export class ScalarFieldBuilder<State extends AnyScalarFieldState = AnyScalarFie
|
|
|
154
158
|
|
|
155
159
|
constructor(private readonly state: State) {}
|
|
156
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
|
+
|
|
157
171
|
optional(): ScalarFieldBuilder<
|
|
158
172
|
State extends ScalarFieldState<
|
|
159
173
|
infer CodecId,
|
|
@@ -357,9 +371,19 @@ function namedTypeField<TypeRef extends StorageTypeInstance>(
|
|
|
357
371
|
function namedTypeField<TypeRef extends PostgresEnumStorageEntry>(
|
|
358
372
|
typeRef: TypeRef,
|
|
359
373
|
): ScalarFieldBuilder<ScalarFieldState<string, TypeRef, false, undefined>>;
|
|
374
|
+
function namedTypeField<Handle extends EnumTypeHandle>(
|
|
375
|
+
typeRef: Handle,
|
|
376
|
+
): ScalarFieldBuilder<ScalarFieldState<Handle['codecId'], Handle, false, undefined>>;
|
|
360
377
|
function namedTypeField(
|
|
361
378
|
typeRef: NamedStorageTypeRef,
|
|
362
379
|
): ScalarFieldBuilder<ScalarFieldState<string, NamedStorageTypeRef, false, undefined>> {
|
|
380
|
+
if (isEnumTypeHandle(typeRef)) {
|
|
381
|
+
return new ScalarFieldBuilder({
|
|
382
|
+
kind: 'scalar',
|
|
383
|
+
typeRef,
|
|
384
|
+
nullable: false,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
363
387
|
return new ScalarFieldBuilder({
|
|
364
388
|
kind: 'scalar',
|
|
365
389
|
typeRef,
|
|
@@ -429,6 +453,22 @@ type BelongsToRelation<
|
|
|
429
453
|
readonly from: FromField;
|
|
430
454
|
readonly to: ToField;
|
|
431
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;
|
|
432
472
|
};
|
|
433
473
|
|
|
434
474
|
type HasManyRelation<
|
|
@@ -520,27 +560,69 @@ export class RelationBuilder<State extends RelationState = AnyRelationState> {
|
|
|
520
560
|
}
|
|
521
561
|
}
|
|
522
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
|
+
*/
|
|
523
570
|
export type ColumnRef<FieldName extends string = string> = {
|
|
524
571
|
readonly kind: 'columnRef';
|
|
525
572
|
readonly fieldName: FieldName;
|
|
526
573
|
};
|
|
527
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
|
+
*/
|
|
528
585
|
export type TargetFieldRef<
|
|
529
586
|
ModelName extends string = string,
|
|
530
587
|
FieldName extends string = string,
|
|
531
|
-
|
|
588
|
+
TSpaceId extends string = string,
|
|
532
589
|
> = {
|
|
533
590
|
readonly kind: 'targetFieldRef';
|
|
534
|
-
readonly source:
|
|
591
|
+
readonly source: TargetFieldRefSource;
|
|
535
592
|
readonly modelName: ModelName;
|
|
536
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;
|
|
537
618
|
};
|
|
538
619
|
|
|
539
620
|
export type ModelTokenRefs<
|
|
540
621
|
ModelName extends string,
|
|
541
622
|
Fields extends Record<string, ScalarFieldBuilder>,
|
|
623
|
+
TSpaceId extends string = '<self>',
|
|
542
624
|
> = {
|
|
543
|
-
readonly [K in keyof Fields]: TargetFieldRef<ModelName, K & string>;
|
|
625
|
+
readonly [K in keyof Fields]: TargetFieldRef<ModelName, K & string, TSpaceId>;
|
|
544
626
|
};
|
|
545
627
|
|
|
546
628
|
type ConstraintOptions<Name extends string | undefined = string | undefined> = {
|
|
@@ -612,6 +694,22 @@ export type ForeignKeyConstraint<
|
|
|
612
694
|
readonly targetModel: TargetModelName;
|
|
613
695
|
readonly targetFields: TargetFieldNames;
|
|
614
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;
|
|
615
713
|
readonly name?: Name;
|
|
616
714
|
readonly onDelete?: 'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault';
|
|
617
715
|
readonly onUpdate?: 'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault';
|
|
@@ -627,6 +725,9 @@ function normalizeTargetFieldRefInput(input: TargetFieldRef | readonly TargetFie
|
|
|
627
725
|
readonly modelName: string;
|
|
628
726
|
readonly fieldNames: readonly string[];
|
|
629
727
|
readonly source: TargetFieldRefSource;
|
|
728
|
+
readonly spaceId: string | undefined;
|
|
729
|
+
readonly namespaceId: string | undefined;
|
|
730
|
+
readonly tableName: string | undefined;
|
|
630
731
|
} {
|
|
631
732
|
const refs = Array.isArray(input) ? input : [input];
|
|
632
733
|
const [first] = refs;
|
|
@@ -636,10 +737,33 @@ function normalizeTargetFieldRefInput(input: TargetFieldRef | readonly TargetFie
|
|
|
636
737
|
if (refs.some((ref) => ref.modelName !== first.modelName)) {
|
|
637
738
|
throw new Error('All target refs in a foreign key must point to the same model');
|
|
638
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
|
+
}
|
|
639
758
|
return {
|
|
640
759
|
modelName: first.modelName,
|
|
641
|
-
|
|
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),
|
|
642
763
|
source: refs.some((ref) => ref.source === 'string') ? 'string' : 'token',
|
|
764
|
+
spaceId: first.spaceId,
|
|
765
|
+
namespaceId: first.namespaceId,
|
|
766
|
+
tableName: first.tableName,
|
|
643
767
|
};
|
|
644
768
|
}
|
|
645
769
|
|
|
@@ -759,6 +883,15 @@ function createConstraintsDsl<IndexTypes extends IndexTypeMap = Record<never, ne
|
|
|
759
883
|
targetModel: normalizedTarget.modelName,
|
|
760
884
|
targetFields: normalizedTarget.fieldNames,
|
|
761
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
|
+
: {}),
|
|
762
895
|
...(options?.name ? { name: options.name } : {}),
|
|
763
896
|
...(options?.onDelete ? { onDelete: options.onDelete } : {}),
|
|
764
897
|
...(options?.onUpdate ? { onUpdate: options.onUpdate } : {}),
|
|
@@ -785,6 +918,7 @@ export type ModelAttributesSpec = {
|
|
|
785
918
|
|
|
786
919
|
export type SqlStageSpec = {
|
|
787
920
|
readonly table?: string;
|
|
921
|
+
readonly control?: ControlPolicy;
|
|
788
922
|
readonly indexes?: readonly IndexConstraint[];
|
|
789
923
|
readonly foreignKeys?: readonly ForeignKeyConstraint[];
|
|
790
924
|
};
|
|
@@ -833,17 +967,40 @@ function createFieldRefs<Fields extends Record<string, ScalarFieldBuilder>>(
|
|
|
833
967
|
function createModelTokenRefs<
|
|
834
968
|
ModelName extends string,
|
|
835
969
|
Fields extends Record<string, ScalarFieldBuilder>,
|
|
836
|
-
|
|
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> {
|
|
837
980
|
const refs = {} as Record<string, TargetFieldRef>;
|
|
838
|
-
for (const fieldName of Object.
|
|
981
|
+
for (const [fieldName, fieldBuilder] of Object.entries(fields)) {
|
|
982
|
+
const physicalColumn =
|
|
983
|
+
crossSpaceCoordinate !== undefined ? fieldBuilder.physicalColumnName : undefined;
|
|
839
984
|
refs[fieldName] = {
|
|
840
985
|
kind: 'targetFieldRef',
|
|
841
986
|
source: 'token',
|
|
842
987
|
modelName,
|
|
843
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
|
+
: {}),
|
|
844
1001
|
};
|
|
845
1002
|
}
|
|
846
|
-
return refs as ModelTokenRefs<ModelName, Fields>;
|
|
1003
|
+
return refs as ModelTokenRefs<ModelName, Fields, TSpaceId>;
|
|
847
1004
|
}
|
|
848
1005
|
|
|
849
1006
|
type StageInput<Context, Spec> = Spec | ((context: Context) => Spec);
|
|
@@ -988,6 +1145,7 @@ export class ContractModelBuilder<
|
|
|
988
1145
|
AttributesSpec extends ModelAttributesSpec | undefined = undefined,
|
|
989
1146
|
SqlSpec extends SqlStageSpec | undefined = undefined,
|
|
990
1147
|
IndexTypes extends IndexTypeMap = Record<never, never>,
|
|
1148
|
+
TSpaceId extends string = '<self>',
|
|
991
1149
|
> {
|
|
992
1150
|
declare readonly __name: ModelName;
|
|
993
1151
|
declare readonly __fields: Fields;
|
|
@@ -995,7 +1153,8 @@ export class ContractModelBuilder<
|
|
|
995
1153
|
declare readonly __attributes: AttributesSpec;
|
|
996
1154
|
declare readonly __sql: SqlSpec;
|
|
997
1155
|
declare readonly __indexTypes: IndexTypes;
|
|
998
|
-
readonly
|
|
1156
|
+
declare readonly __spaceId: TSpaceId;
|
|
1157
|
+
readonly refs: ModelName extends string ? ModelTokenRefs<ModelName, Fields, TSpaceId> : never;
|
|
999
1158
|
|
|
1000
1159
|
constructor(
|
|
1001
1160
|
readonly stageOne: {
|
|
@@ -1006,10 +1165,25 @@ export class ContractModelBuilder<
|
|
|
1006
1165
|
},
|
|
1007
1166
|
readonly attributesFactory?: StageInput<AttributeContext<Fields>, AttributesSpec>,
|
|
1008
1167
|
readonly sqlFactory?: StageInput<SqlContext<Fields, IndexTypes>, SqlSpec>,
|
|
1168
|
+
readonly spaceId?: TSpaceId,
|
|
1169
|
+
readonly tableName?: string,
|
|
1009
1170
|
) {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
+
);
|
|
1013
1187
|
}
|
|
1014
1188
|
|
|
1015
1189
|
ref<FieldName extends keyof Fields & string>(
|
|
@@ -1039,7 +1213,8 @@ export class ContractModelBuilder<
|
|
|
1039
1213
|
Relations & NextRelations,
|
|
1040
1214
|
AttributesSpec,
|
|
1041
1215
|
SqlSpec,
|
|
1042
|
-
IndexTypes
|
|
1216
|
+
IndexTypes,
|
|
1217
|
+
TSpaceId
|
|
1043
1218
|
> {
|
|
1044
1219
|
const duplicateRelationName = findDuplicateRelationName(this.stageOne.relations, relations);
|
|
1045
1220
|
if (duplicateRelationName) {
|
|
@@ -1058,6 +1233,8 @@ export class ContractModelBuilder<
|
|
|
1058
1233
|
},
|
|
1059
1234
|
this.attributesFactory,
|
|
1060
1235
|
this.sqlFactory,
|
|
1236
|
+
this.spaceId,
|
|
1237
|
+
this.tableName,
|
|
1061
1238
|
);
|
|
1062
1239
|
}
|
|
1063
1240
|
|
|
@@ -1066,17 +1243,61 @@ export class ContractModelBuilder<
|
|
|
1066
1243
|
AttributeContext<Fields>,
|
|
1067
1244
|
ValidateAttributesStageSpec<Fields, SqlSpec, NextAttributesSpec>
|
|
1068
1245
|
>,
|
|
1069
|
-
): ContractModelBuilder<
|
|
1070
|
-
|
|
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
|
+
);
|
|
1071
1262
|
}
|
|
1072
1263
|
|
|
1073
1264
|
sql<const NextSqlSpec extends SqlStageSpec>(
|
|
1074
1265
|
specOrFactory: StageInput<SqlContext<Fields, IndexTypes>, NextSqlSpec>,
|
|
1075
1266
|
): [ValidateSqlStageSpec<Fields, AttributesSpec, NextSqlSpec>] extends [never]
|
|
1076
|
-
? ContractModelBuilder<
|
|
1077
|
-
|
|
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
|
+
> {
|
|
1078
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).
|
|
1079
|
-
|
|
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
|
+
);
|
|
1080
1301
|
}
|
|
1081
1302
|
|
|
1082
1303
|
buildAttributesSpec(): AttributesSpec {
|
|
@@ -1216,6 +1437,7 @@ export type ContractInput<
|
|
|
1216
1437
|
readonly naming?: NamingConfig;
|
|
1217
1438
|
readonly storageHash?: string;
|
|
1218
1439
|
readonly foreignKeyDefaults?: ForeignKeyDefaultsState;
|
|
1440
|
+
readonly defaultControlPolicy?: ControlPolicy;
|
|
1219
1441
|
/**
|
|
1220
1442
|
* Declared namespace coordinates the contract recognises. Per-model
|
|
1221
1443
|
* `namespace` references must reference an entry in this list (or the
|
|
@@ -1264,6 +1486,12 @@ export type ContractInput<
|
|
|
1264
1486
|
readonly types?: Types;
|
|
1265
1487
|
readonly models?: Models;
|
|
1266
1488
|
readonly codecLookup?: CodecLookup;
|
|
1489
|
+
/**
|
|
1490
|
+
* Domain enum handles authored via `enumType()`. Each handle lowers to a
|
|
1491
|
+
* domain `enum` entry and a storage `valueSet` entry in the target's
|
|
1492
|
+
* default namespace. Fields reference the enum via `field.namedType(handle)`.
|
|
1493
|
+
*/
|
|
1494
|
+
readonly enums?: Record<string, import('./enum-type').EnumTypeHandle>;
|
|
1267
1495
|
};
|
|
1268
1496
|
|
|
1269
1497
|
export function model<
|
|
@@ -1320,6 +1548,86 @@ export function model<
|
|
|
1320
1548
|
});
|
|
1321
1549
|
}
|
|
1322
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
|
+
|
|
1323
1631
|
function belongsTo<
|
|
1324
1632
|
Token extends AnyNamedModelToken,
|
|
1325
1633
|
FromField extends string | readonly string[],
|
|
@@ -1343,11 +1651,31 @@ function belongsTo(
|
|
|
1343
1651
|
readonly to: string | readonly string[];
|
|
1344
1652
|
},
|
|
1345
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
|
+
|
|
1346
1673
|
return new RelationBuilder({
|
|
1347
1674
|
kind: 'belongsTo',
|
|
1348
1675
|
toModel: normalizeRelationModelSource(toModel),
|
|
1349
1676
|
from: options.from,
|
|
1350
1677
|
to: options.to,
|
|
1678
|
+
...(crossSpaceCoordinate !== undefined ? crossSpaceCoordinate : {}),
|
|
1351
1679
|
});
|
|
1352
1680
|
}
|
|
1353
1681
|
|