@prisma-next/contract-authoring 0.3.0-dev.4 → 0.3.0-dev.40
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 +4 -1
- package/dist/index.d.mts +295 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +226 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +24 -15
- package/src/builder-state.ts +188 -0
- package/src/contract-builder.ts +169 -0
- package/src/index.ts +31 -0
- package/src/model-builder.ts +160 -0
- package/src/table-builder.ts +309 -0
- package/src/types.ts +131 -0
- package/dist/index.d.ts +0 -175
- package/dist/index.js +0 -179
- package/dist/index.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,36 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/contract-authoring",
|
|
3
|
-
"version": "0.3.0-dev.
|
|
3
|
+
"version": "0.3.0-dev.40",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Target-agnostic contract authoring builder core for Prisma Next",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"ts-toolbelt": "^9.6.0",
|
|
9
|
-
"@prisma-next/
|
|
9
|
+
"@prisma-next/utils": "0.3.0-dev.40",
|
|
10
|
+
"@prisma-next/contract": "0.3.0-dev.40"
|
|
10
11
|
},
|
|
11
12
|
"devDependencies": {
|
|
12
|
-
"
|
|
13
|
-
"tsup": "8.5.1",
|
|
13
|
+
"tsdown": "0.18.4",
|
|
14
14
|
"typescript": "5.9.3",
|
|
15
|
-
"vitest": "4.0.
|
|
15
|
+
"vitest": "4.0.17",
|
|
16
|
+
"@prisma-next/tsconfig": "0.0.0",
|
|
17
|
+
"@prisma-next/tsdown": "0.0.0"
|
|
16
18
|
},
|
|
17
19
|
"files": [
|
|
18
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"src"
|
|
19
22
|
],
|
|
20
23
|
"exports": {
|
|
21
|
-
".":
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
".": "./dist/index.mjs",
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"main": "./dist/index.mjs",
|
|
28
|
+
"module": "./dist/index.mjs",
|
|
29
|
+
"types": "./dist/index.d.mts",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/prisma/prisma-next.git",
|
|
33
|
+
"directory": "packages/1-framework/2-authoring/contract"
|
|
25
34
|
},
|
|
26
35
|
"scripts": {
|
|
27
|
-
"build": "
|
|
36
|
+
"build": "tsdown",
|
|
28
37
|
"test": "vitest run",
|
|
29
38
|
"test:coverage": "vitest run --coverage",
|
|
30
39
|
"typecheck": "tsc --project tsconfig.json --noEmit",
|
|
31
|
-
"lint": "biome check . --
|
|
32
|
-
"lint:fix": "biome check --write .
|
|
33
|
-
"lint:fix:unsafe": "biome check --write --unsafe .
|
|
34
|
-
"clean": "
|
|
40
|
+
"lint": "biome check . --error-on-warnings",
|
|
41
|
+
"lint:fix": "biome check --write .",
|
|
42
|
+
"lint:fix:unsafe": "biome check --write --unsafe .",
|
|
43
|
+
"clean": "rm -rf dist dist-tsc dist-tsc-prod coverage .tmp-output"
|
|
35
44
|
}
|
|
36
45
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { ColumnDefault, ExecutionMutationDefaultValue } from '@prisma-next/contract/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Column type descriptor containing both codec ID and native type.
|
|
5
|
+
* Used when defining columns with descriptor objects instead of string IDs.
|
|
6
|
+
*
|
|
7
|
+
* For parameterized types (e.g., `vector(1536)`), the `typeParams` field
|
|
8
|
+
* carries codec-owned parameters that affect both TypeScript type generation
|
|
9
|
+
* and native DDL output.
|
|
10
|
+
*/
|
|
11
|
+
export type ColumnTypeDescriptor = {
|
|
12
|
+
readonly codecId: string;
|
|
13
|
+
readonly nativeType: string;
|
|
14
|
+
readonly typeParams?: Record<string, unknown>;
|
|
15
|
+
readonly typeRef?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Base column properties shared by all column states.
|
|
20
|
+
*/
|
|
21
|
+
type ColumnBuilderStateBase<Name extends string, Type extends string> = {
|
|
22
|
+
readonly name: Name;
|
|
23
|
+
readonly type: Type;
|
|
24
|
+
readonly nativeType: string;
|
|
25
|
+
readonly typeParams?: Record<string, unknown>;
|
|
26
|
+
readonly typeRef?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type StorageTypeInstanceState = {
|
|
30
|
+
readonly codecId: string;
|
|
31
|
+
readonly nativeType: string;
|
|
32
|
+
readonly typeParams: Record<string, unknown>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Descriptive error type shown when attempting to use both nullable and default.
|
|
37
|
+
* This string literal appears in TypeScript error messages for better DX.
|
|
38
|
+
*/
|
|
39
|
+
export type NullableColumnCannotHaveDefault =
|
|
40
|
+
"Error: A nullable column cannot have a default value. Remove 'nullable: true' or remove 'default'.";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Column builder state with enforced nullable/default mutual exclusivity.
|
|
44
|
+
*
|
|
45
|
+
* Invariant: A column with a default value is always NOT NULL.
|
|
46
|
+
* This is enforced at the type level via conditional types:
|
|
47
|
+
* - Nullable columns (`nullable: true`) cannot have a `default` property
|
|
48
|
+
* - Non-nullable columns (`nullable: false`) can optionally have a `default` property
|
|
49
|
+
*/
|
|
50
|
+
export type ColumnBuilderState<
|
|
51
|
+
Name extends string,
|
|
52
|
+
Nullable extends boolean,
|
|
53
|
+
Type extends string,
|
|
54
|
+
> = ColumnBuilderStateBase<Name, Type> &
|
|
55
|
+
(Nullable extends true
|
|
56
|
+
? { readonly nullable: true; readonly default?: NullableColumnCannotHaveDefault }
|
|
57
|
+
: {
|
|
58
|
+
readonly nullable: false;
|
|
59
|
+
readonly default?: ColumnDefault;
|
|
60
|
+
readonly executionDefault?: ExecutionMutationDefaultValue;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Unique constraint definition for table builder.
|
|
65
|
+
*/
|
|
66
|
+
export interface UniqueConstraintDef {
|
|
67
|
+
readonly columns: readonly string[];
|
|
68
|
+
readonly name?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Index definition for table builder.
|
|
73
|
+
*/
|
|
74
|
+
export interface IndexDef {
|
|
75
|
+
readonly columns: readonly string[];
|
|
76
|
+
readonly name?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Foreign key definition for table builder.
|
|
81
|
+
*/
|
|
82
|
+
export interface ForeignKeyDef {
|
|
83
|
+
readonly columns: readonly string[];
|
|
84
|
+
readonly references: {
|
|
85
|
+
readonly table: string;
|
|
86
|
+
readonly columns: readonly string[];
|
|
87
|
+
};
|
|
88
|
+
readonly name?: string;
|
|
89
|
+
readonly constraint?: boolean;
|
|
90
|
+
readonly index?: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface TableBuilderState<
|
|
94
|
+
Name extends string,
|
|
95
|
+
Columns extends Record<string, ColumnBuilderState<string, boolean, string>>,
|
|
96
|
+
PrimaryKey extends readonly string[] | undefined,
|
|
97
|
+
> {
|
|
98
|
+
readonly name: Name;
|
|
99
|
+
readonly columns: Columns;
|
|
100
|
+
readonly primaryKey?: PrimaryKey;
|
|
101
|
+
readonly primaryKeyName?: string;
|
|
102
|
+
readonly uniques: readonly UniqueConstraintDef[];
|
|
103
|
+
readonly indexes: readonly IndexDef[];
|
|
104
|
+
readonly foreignKeys: readonly ForeignKeyDef[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type RelationDefinition = {
|
|
108
|
+
readonly to: string;
|
|
109
|
+
readonly cardinality: '1:1' | '1:N' | 'N:1' | 'N:M';
|
|
110
|
+
readonly on: {
|
|
111
|
+
readonly parentCols: readonly string[];
|
|
112
|
+
readonly childCols: readonly string[];
|
|
113
|
+
};
|
|
114
|
+
readonly through?: {
|
|
115
|
+
readonly table: string;
|
|
116
|
+
readonly parentCols: readonly string[];
|
|
117
|
+
readonly childCols: readonly string[];
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export interface ModelBuilderState<
|
|
122
|
+
Name extends string,
|
|
123
|
+
Table extends string,
|
|
124
|
+
Fields extends Record<string, string>,
|
|
125
|
+
Relations extends Record<string, RelationDefinition>,
|
|
126
|
+
> {
|
|
127
|
+
readonly name: Name;
|
|
128
|
+
readonly table: Table;
|
|
129
|
+
readonly fields: Fields;
|
|
130
|
+
readonly relations: Relations;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface ForeignKeyDefaultsState {
|
|
134
|
+
readonly constraint: boolean;
|
|
135
|
+
readonly index: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface ContractBuilderState<
|
|
139
|
+
Target extends string | undefined = string | undefined,
|
|
140
|
+
Tables extends Record<
|
|
141
|
+
string,
|
|
142
|
+
TableBuilderState<
|
|
143
|
+
string,
|
|
144
|
+
Record<string, ColumnBuilderState<string, boolean, string>>,
|
|
145
|
+
readonly string[] | undefined
|
|
146
|
+
>
|
|
147
|
+
> = Record<
|
|
148
|
+
never,
|
|
149
|
+
TableBuilderState<
|
|
150
|
+
string,
|
|
151
|
+
Record<string, ColumnBuilderState<string, boolean, string>>,
|
|
152
|
+
readonly string[] | undefined
|
|
153
|
+
>
|
|
154
|
+
>,
|
|
155
|
+
Models extends Record<
|
|
156
|
+
string,
|
|
157
|
+
ModelBuilderState<string, string, Record<string, string>, Record<string, RelationDefinition>>
|
|
158
|
+
> = Record<
|
|
159
|
+
never,
|
|
160
|
+
ModelBuilderState<string, string, Record<string, string>, Record<string, RelationDefinition>>
|
|
161
|
+
>,
|
|
162
|
+
StorageHash extends string | undefined = string | undefined,
|
|
163
|
+
ExtensionPacks extends Record<string, unknown> | undefined = undefined,
|
|
164
|
+
Capabilities extends Record<string, Record<string, boolean>> | undefined = undefined,
|
|
165
|
+
> {
|
|
166
|
+
readonly target?: Target;
|
|
167
|
+
readonly tables: Tables;
|
|
168
|
+
readonly models: Models;
|
|
169
|
+
readonly storageHash?: StorageHash;
|
|
170
|
+
readonly extensionPacks?: ExtensionPacks;
|
|
171
|
+
readonly capabilities?: Capabilities;
|
|
172
|
+
readonly storageTypes?: Record<string, StorageTypeInstanceState>;
|
|
173
|
+
readonly foreignKeyDefaults?: ForeignKeyDefaultsState;
|
|
174
|
+
/**
|
|
175
|
+
* Array of extension pack namespace identifiers (e.g., ['pgvector', 'postgis']).
|
|
176
|
+
* Populated when extension packs are registered during contract building.
|
|
177
|
+
* Used to track which extension packs are included in the contract.
|
|
178
|
+
* Can be undefined or empty if no extension packs are registered.
|
|
179
|
+
* Namespace format matches the extension pack ID (e.g., 'pgvector', not 'pgvector@1.0.0').
|
|
180
|
+
*/
|
|
181
|
+
readonly extensionNamespaces?: readonly string[];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface ColumnBuilder<Name extends string, Nullable extends boolean, Type extends string> {
|
|
185
|
+
nullable<Value extends boolean>(value?: Value): ColumnBuilder<Name, Value, Type>;
|
|
186
|
+
type<Id extends string>(id: Id): ColumnBuilder<Name, Nullable, Id>;
|
|
187
|
+
build(): ColumnBuilderState<Name, Nullable, Type>;
|
|
188
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { TargetPackRef } from '@prisma-next/contract/framework-components';
|
|
2
|
+
import type {
|
|
3
|
+
ColumnBuilderState,
|
|
4
|
+
ContractBuilderState,
|
|
5
|
+
ForeignKeyDefaultsState,
|
|
6
|
+
ModelBuilderState,
|
|
7
|
+
RelationDefinition,
|
|
8
|
+
TableBuilderState,
|
|
9
|
+
} from './builder-state';
|
|
10
|
+
import { ModelBuilder } from './model-builder';
|
|
11
|
+
import { createTable, TableBuilder } from './table-builder';
|
|
12
|
+
|
|
13
|
+
export class ContractBuilder<
|
|
14
|
+
Target extends string | undefined = undefined,
|
|
15
|
+
Tables extends Record<
|
|
16
|
+
string,
|
|
17
|
+
TableBuilderState<
|
|
18
|
+
string,
|
|
19
|
+
Record<string, ColumnBuilderState<string, boolean, string>>,
|
|
20
|
+
readonly string[] | undefined
|
|
21
|
+
>
|
|
22
|
+
> = Record<never, never>,
|
|
23
|
+
Models extends Record<
|
|
24
|
+
string,
|
|
25
|
+
ModelBuilderState<string, string, Record<string, string>, Record<string, RelationDefinition>>
|
|
26
|
+
> = Record<never, never>,
|
|
27
|
+
StorageHash extends string | undefined = undefined,
|
|
28
|
+
ExtensionPacks extends Record<string, unknown> | undefined = undefined,
|
|
29
|
+
Capabilities extends Record<string, Record<string, boolean>> | undefined = undefined,
|
|
30
|
+
> {
|
|
31
|
+
protected readonly state: ContractBuilderState<
|
|
32
|
+
Target,
|
|
33
|
+
Tables,
|
|
34
|
+
Models,
|
|
35
|
+
StorageHash,
|
|
36
|
+
ExtensionPacks,
|
|
37
|
+
Capabilities
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
state?: ContractBuilderState<Target, Tables, Models, StorageHash, ExtensionPacks, Capabilities>,
|
|
42
|
+
) {
|
|
43
|
+
this.state =
|
|
44
|
+
state ??
|
|
45
|
+
({
|
|
46
|
+
tables: {},
|
|
47
|
+
models: {},
|
|
48
|
+
} as ContractBuilderState<Target, Tables, Models, StorageHash, ExtensionPacks, Capabilities>);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
target<T extends string>(
|
|
52
|
+
packRef: TargetPackRef<string, T>,
|
|
53
|
+
): ContractBuilder<T, Tables, Models, StorageHash, ExtensionPacks, Capabilities> {
|
|
54
|
+
return new ContractBuilder<T, Tables, Models, StorageHash, ExtensionPacks, Capabilities>({
|
|
55
|
+
...this.state,
|
|
56
|
+
target: packRef.targetId,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
capabilities<C extends Record<string, Record<string, boolean>>>(
|
|
61
|
+
capabilities: C,
|
|
62
|
+
): ContractBuilder<Target, Tables, Models, StorageHash, ExtensionPacks, C> {
|
|
63
|
+
return new ContractBuilder<Target, Tables, Models, StorageHash, ExtensionPacks, C>({
|
|
64
|
+
...this.state,
|
|
65
|
+
capabilities,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
table<
|
|
70
|
+
TableName extends string,
|
|
71
|
+
T extends TableBuilder<
|
|
72
|
+
TableName,
|
|
73
|
+
Record<string, ColumnBuilderState<string, boolean, string>>,
|
|
74
|
+
readonly string[] | undefined
|
|
75
|
+
>,
|
|
76
|
+
>(
|
|
77
|
+
name: TableName,
|
|
78
|
+
callback: (t: TableBuilder<TableName>) => T | undefined,
|
|
79
|
+
): ContractBuilder<
|
|
80
|
+
Target,
|
|
81
|
+
Tables & Record<TableName, ReturnType<T['build']>>,
|
|
82
|
+
Models,
|
|
83
|
+
StorageHash,
|
|
84
|
+
ExtensionPacks,
|
|
85
|
+
Capabilities
|
|
86
|
+
> {
|
|
87
|
+
const tableBuilder = createTable(name);
|
|
88
|
+
const result = callback(tableBuilder);
|
|
89
|
+
const finalBuilder = result instanceof TableBuilder ? result : tableBuilder;
|
|
90
|
+
const tableState = finalBuilder.build();
|
|
91
|
+
|
|
92
|
+
return new ContractBuilder<
|
|
93
|
+
Target,
|
|
94
|
+
Tables & Record<TableName, ReturnType<T['build']>>,
|
|
95
|
+
Models,
|
|
96
|
+
StorageHash,
|
|
97
|
+
ExtensionPacks,
|
|
98
|
+
Capabilities
|
|
99
|
+
>({
|
|
100
|
+
...this.state,
|
|
101
|
+
tables: { ...this.state.tables, [name]: tableState } as Tables &
|
|
102
|
+
Record<TableName, ReturnType<T['build']>>,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
model<
|
|
107
|
+
ModelName extends string,
|
|
108
|
+
TableName extends string,
|
|
109
|
+
M extends ModelBuilder<
|
|
110
|
+
ModelName,
|
|
111
|
+
TableName,
|
|
112
|
+
Record<string, string>,
|
|
113
|
+
Record<string, RelationDefinition>
|
|
114
|
+
>,
|
|
115
|
+
>(
|
|
116
|
+
name: ModelName,
|
|
117
|
+
table: TableName,
|
|
118
|
+
callback: (
|
|
119
|
+
m: ModelBuilder<ModelName, TableName, Record<string, string>, Record<never, never>>,
|
|
120
|
+
) => M | undefined,
|
|
121
|
+
): ContractBuilder<
|
|
122
|
+
Target,
|
|
123
|
+
Tables,
|
|
124
|
+
Models & Record<ModelName, ReturnType<M['build']>>,
|
|
125
|
+
StorageHash,
|
|
126
|
+
ExtensionPacks,
|
|
127
|
+
Capabilities
|
|
128
|
+
> {
|
|
129
|
+
const modelBuilder = new ModelBuilder<ModelName, TableName>(name, table);
|
|
130
|
+
const result = callback(modelBuilder);
|
|
131
|
+
const finalBuilder = result instanceof ModelBuilder ? result : modelBuilder;
|
|
132
|
+
const modelState = finalBuilder.build();
|
|
133
|
+
|
|
134
|
+
return new ContractBuilder<
|
|
135
|
+
Target,
|
|
136
|
+
Tables,
|
|
137
|
+
Models & Record<ModelName, ReturnType<M['build']>>,
|
|
138
|
+
StorageHash,
|
|
139
|
+
ExtensionPacks,
|
|
140
|
+
Capabilities
|
|
141
|
+
>({
|
|
142
|
+
...this.state,
|
|
143
|
+
models: { ...this.state.models, [name]: modelState } as Models &
|
|
144
|
+
Record<ModelName, ReturnType<M['build']>>,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
storageHash<H extends string>(
|
|
149
|
+
hash: H,
|
|
150
|
+
): ContractBuilder<Target, Tables, Models, H, ExtensionPacks, Capabilities> {
|
|
151
|
+
return new ContractBuilder<Target, Tables, Models, H, ExtensionPacks, Capabilities>({
|
|
152
|
+
...this.state,
|
|
153
|
+
storageHash: hash,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
foreignKeyDefaults(
|
|
158
|
+
config: ForeignKeyDefaultsState,
|
|
159
|
+
): ContractBuilder<Target, Tables, Models, StorageHash, ExtensionPacks, Capabilities> {
|
|
160
|
+
return new ContractBuilder<Target, Tables, Models, StorageHash, ExtensionPacks, Capabilities>({
|
|
161
|
+
...this.state,
|
|
162
|
+
foreignKeyDefaults: config,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function defineContract(): ContractBuilder {
|
|
168
|
+
return new ContractBuilder();
|
|
169
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
ColumnBuilder,
|
|
3
|
+
ColumnBuilderState,
|
|
4
|
+
ColumnTypeDescriptor,
|
|
5
|
+
ContractBuilderState,
|
|
6
|
+
ForeignKeyDef,
|
|
7
|
+
ForeignKeyDefaultsState,
|
|
8
|
+
IndexDef,
|
|
9
|
+
ModelBuilderState,
|
|
10
|
+
RelationDefinition,
|
|
11
|
+
TableBuilderState,
|
|
12
|
+
UniqueConstraintDef,
|
|
13
|
+
} from './builder-state';
|
|
14
|
+
|
|
15
|
+
export { ContractBuilder, defineContract } from './contract-builder';
|
|
16
|
+
export { ModelBuilder } from './model-builder';
|
|
17
|
+
export { createTable, TableBuilder } from './table-builder';
|
|
18
|
+
|
|
19
|
+
export type {
|
|
20
|
+
BuildModelFields,
|
|
21
|
+
BuildModels,
|
|
22
|
+
BuildRelations,
|
|
23
|
+
BuildStorage,
|
|
24
|
+
BuildStorageColumn,
|
|
25
|
+
BuildStorageTables,
|
|
26
|
+
ExtractColumns,
|
|
27
|
+
ExtractModelFields,
|
|
28
|
+
ExtractModelRelations,
|
|
29
|
+
ExtractPrimaryKey,
|
|
30
|
+
Mutable,
|
|
31
|
+
} from './types';
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { ModelBuilderState, RelationDefinition } from './builder-state';
|
|
2
|
+
|
|
3
|
+
export class ModelBuilder<
|
|
4
|
+
Name extends string,
|
|
5
|
+
Table extends string,
|
|
6
|
+
Fields extends Record<string, string> = Record<never, never>,
|
|
7
|
+
Relations extends Record<string, RelationDefinition> = Record<never, never>,
|
|
8
|
+
> {
|
|
9
|
+
private readonly _name: Name;
|
|
10
|
+
private readonly _table: Table;
|
|
11
|
+
private readonly _fields: Fields;
|
|
12
|
+
private readonly _relations: Relations;
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
name: Name,
|
|
16
|
+
table: Table,
|
|
17
|
+
fields: Fields = {} as Fields,
|
|
18
|
+
relations: Relations = {} as Relations,
|
|
19
|
+
) {
|
|
20
|
+
this._name = name;
|
|
21
|
+
this._table = table;
|
|
22
|
+
this._fields = fields;
|
|
23
|
+
this._relations = relations;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
field<FieldName extends string, ColumnName extends string>(
|
|
27
|
+
fieldName: FieldName,
|
|
28
|
+
columnName: ColumnName,
|
|
29
|
+
): ModelBuilder<Name, Table, Fields & Record<FieldName, ColumnName>, Relations> {
|
|
30
|
+
return new ModelBuilder(
|
|
31
|
+
this._name,
|
|
32
|
+
this._table,
|
|
33
|
+
{
|
|
34
|
+
...this._fields,
|
|
35
|
+
[fieldName]: columnName,
|
|
36
|
+
} as Fields & Record<FieldName, ColumnName>,
|
|
37
|
+
this._relations,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
relation<RelationName extends string, ToModel extends string, ToTable extends string>(
|
|
42
|
+
name: RelationName,
|
|
43
|
+
options: {
|
|
44
|
+
toModel: ToModel;
|
|
45
|
+
toTable: ToTable;
|
|
46
|
+
cardinality: '1:1' | '1:N' | 'N:1';
|
|
47
|
+
on: {
|
|
48
|
+
parentTable: Table;
|
|
49
|
+
parentColumns: readonly string[];
|
|
50
|
+
childTable: ToTable;
|
|
51
|
+
childColumns: readonly string[];
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
): ModelBuilder<Name, Table, Fields, Relations & Record<RelationName, RelationDefinition>>;
|
|
55
|
+
relation<
|
|
56
|
+
RelationName extends string,
|
|
57
|
+
ToModel extends string,
|
|
58
|
+
ToTable extends string,
|
|
59
|
+
JunctionTable extends string,
|
|
60
|
+
>(
|
|
61
|
+
name: RelationName,
|
|
62
|
+
options: {
|
|
63
|
+
toModel: ToModel;
|
|
64
|
+
toTable: ToTable;
|
|
65
|
+
cardinality: 'N:M';
|
|
66
|
+
through: {
|
|
67
|
+
table: JunctionTable;
|
|
68
|
+
parentColumns: readonly string[];
|
|
69
|
+
childColumns: readonly string[];
|
|
70
|
+
};
|
|
71
|
+
on: {
|
|
72
|
+
parentTable: Table;
|
|
73
|
+
parentColumns: readonly string[];
|
|
74
|
+
childTable: JunctionTable;
|
|
75
|
+
childColumns: readonly string[];
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
): ModelBuilder<Name, Table, Fields, Relations & Record<RelationName, RelationDefinition>>;
|
|
79
|
+
relation<
|
|
80
|
+
RelationName extends string,
|
|
81
|
+
ToModel extends string,
|
|
82
|
+
ToTable extends string,
|
|
83
|
+
JunctionTable extends string = never,
|
|
84
|
+
>(
|
|
85
|
+
name: RelationName,
|
|
86
|
+
options: {
|
|
87
|
+
toModel: ToModel;
|
|
88
|
+
toTable: ToTable;
|
|
89
|
+
cardinality: '1:1' | '1:N' | 'N:1' | 'N:M';
|
|
90
|
+
through?: {
|
|
91
|
+
table: JunctionTable;
|
|
92
|
+
parentColumns: readonly string[];
|
|
93
|
+
childColumns: readonly string[];
|
|
94
|
+
};
|
|
95
|
+
on: {
|
|
96
|
+
parentTable: Table;
|
|
97
|
+
parentColumns: readonly string[];
|
|
98
|
+
childTable: ToTable | JunctionTable;
|
|
99
|
+
childColumns: readonly string[];
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
): ModelBuilder<Name, Table, Fields, Relations & Record<RelationName, RelationDefinition>> {
|
|
103
|
+
// Validate parentTable matches model's table
|
|
104
|
+
if (options.on.parentTable !== this._table) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Relation "${name}" parentTable "${options.on.parentTable}" does not match model table "${this._table}"`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Validate childTable matches toTable (for non-N:M) or through.table (for N:M)
|
|
111
|
+
if (options.cardinality === 'N:M') {
|
|
112
|
+
if (!options.through) {
|
|
113
|
+
throw new Error(`Relation "${name}" with cardinality "N:M" requires through field`);
|
|
114
|
+
}
|
|
115
|
+
if (options.on.childTable !== options.through.table) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Relation "${name}" childTable "${options.on.childTable}" does not match through.table "${options.through.table}"`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
if (options.on.childTable !== options.toTable) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Relation "${name}" childTable "${options.on.childTable}" does not match toTable "${options.toTable}"`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const relationDef: RelationDefinition = {
|
|
129
|
+
to: options.toModel,
|
|
130
|
+
cardinality: options.cardinality,
|
|
131
|
+
on: {
|
|
132
|
+
parentCols: options.on.parentColumns,
|
|
133
|
+
childCols: options.on.childColumns,
|
|
134
|
+
},
|
|
135
|
+
...(options.through
|
|
136
|
+
? {
|
|
137
|
+
through: {
|
|
138
|
+
table: options.through.table,
|
|
139
|
+
parentCols: options.through.parentColumns,
|
|
140
|
+
childCols: options.through.childColumns,
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
: undefined),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return new ModelBuilder(this._name, this._table, this._fields, {
|
|
147
|
+
...this._relations,
|
|
148
|
+
[name]: relationDef,
|
|
149
|
+
} as Relations & Record<RelationName, RelationDefinition>);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
build(): ModelBuilderState<Name, Table, Fields, Relations> {
|
|
153
|
+
return {
|
|
154
|
+
name: this._name,
|
|
155
|
+
table: this._table,
|
|
156
|
+
fields: this._fields,
|
|
157
|
+
relations: this._relations,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|