@prisma-next/emitter 0.3.0-dev.127 → 0.3.0-dev.129
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 +6 -1
- package/dist/domain-type-generation.d.mts +18 -0
- package/dist/domain-type-generation.d.mts.map +1 -0
- package/dist/domain-type-generation.mjs +74 -0
- package/dist/domain-type-generation.mjs.map +1 -0
- package/dist/exports/index.d.mts +2 -1
- package/dist/exports/index.mjs +2 -1
- package/package.json +10 -6
- package/src/domain-type-generation.ts +126 -0
- package/src/exports/index.ts +10 -0
- package/test/canonicalization.test.ts +61 -0
- package/test/domain-type-generation.test.ts +271 -0
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ flowchart TD
|
|
|
49
49
|
|
|
50
50
|
subgraph "Target Family Hooks"
|
|
51
51
|
SQL[SQL Hook]
|
|
52
|
+
MONGO[Mongo Hook]
|
|
52
53
|
DOC[Document Hook]
|
|
53
54
|
end
|
|
54
55
|
|
|
@@ -61,8 +62,10 @@ flowchart TD
|
|
|
61
62
|
TS --> IR
|
|
62
63
|
IR --> VAL
|
|
63
64
|
VAL --> SQL
|
|
65
|
+
VAL --> MONGO
|
|
64
66
|
VAL --> DOC
|
|
65
67
|
SQL --> HASH
|
|
68
|
+
MONGO --> HASH
|
|
66
69
|
DOC --> HASH
|
|
67
70
|
PACK1 --> SQL
|
|
68
71
|
PACK2 --> DOC
|
|
@@ -137,6 +140,7 @@ import { createOperationRegistry } from '@prisma-next/operations';
|
|
|
137
140
|
|
|
138
141
|
// Determine target family SPI based on target family
|
|
139
142
|
import { sqlTargetFamilyHook } from '@prisma-next/sql-contract-emitter';
|
|
143
|
+
// or: import { mongoTargetFamilyHook } from '@prisma-next/mongo-emitter';
|
|
140
144
|
|
|
141
145
|
// Emit contract
|
|
142
146
|
const ir: ContractIR = {
|
|
@@ -188,5 +192,6 @@ This ensures all required fields are present with sensible defaults. See `.curso
|
|
|
188
192
|
|
|
189
193
|
## Exports
|
|
190
194
|
|
|
191
|
-
- `.`: Main emitter API (`emit`, types)
|
|
195
|
+
- `.`: Main emitter API (`emit`, types, shared domain-level generation utilities)
|
|
196
|
+
- `./domain-type-generation`: Shared domain-level `.d.ts` generation utilities (used by family-specific emitter hooks)
|
|
192
197
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TypesImportSpec } from "@prisma-next/contract/types";
|
|
2
|
+
|
|
3
|
+
//#region src/domain-type-generation.d.ts
|
|
4
|
+
declare function serializeValue(value: unknown): string;
|
|
5
|
+
declare function serializeObjectKey(key: string): string;
|
|
6
|
+
declare function generateRootsType(roots: Record<string, string> | undefined): string;
|
|
7
|
+
declare function generateModelRelationsType(relations: Record<string, unknown>): string;
|
|
8
|
+
declare function deduplicateImports(imports: TypesImportSpec[]): TypesImportSpec[];
|
|
9
|
+
declare function generateImportLines(imports: TypesImportSpec[]): string[];
|
|
10
|
+
declare function generateCodecTypeIntersection(imports: ReadonlyArray<TypesImportSpec>, named: string): string;
|
|
11
|
+
declare function generateHashTypeAliases(hashes: {
|
|
12
|
+
readonly storageHash: string;
|
|
13
|
+
readonly executionHash?: string;
|
|
14
|
+
readonly profileHash: string;
|
|
15
|
+
}): string;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { deduplicateImports, generateCodecTypeIntersection, generateHashTypeAliases, generateImportLines, generateModelRelationsType, generateRootsType, serializeObjectKey, serializeValue };
|
|
18
|
+
//# sourceMappingURL=domain-type-generation.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-type-generation.d.mts","names":[],"sources":["../src/domain-type-generation.ts"],"sourcesContent":[],"mappings":";;;iBAEgB,cAAA;iBA+BA,kBAAA;AA/BA,iBAsCA,iBAAA,CAtCc,KAAA,EAsCW,MAtCX,CAAA,MAAA,EAAA,MAAA,CAAA,GAAA,SAAA,CAAA,EAAA,MAAA;AA+Bd,iBAiBA,0BAAA,CAjBkB,SAAA,EAiBoB,MAjBpB,CAAA,MAAA,EAAA,OAAA,CAAA,CAAA,EAAA,MAAA;AAOlB,iBA2CA,kBAAA,CA3C+B,OAAA,EA2CH,eA3CG,EAAA,CAAA,EA2CiB,eA3CjB,EAAA;AAU/B,iBA8CA,mBAAA,CA9CsC,OAAM,EA8Cf,eA9Ce,EAAA,CAAA,EAAA,MAAA,EAAA;AAiC5C,iBAoBA,6BAAA,CApBgD,OAAA,EAqBrD,aArBoE,CAqBtD,eArBsD,CAAA,EAAA,KAAA,EAAA,MAAA,CAAA,EAAA,MAAA;AAa/D,iBAeA,uBAAA,CAf6B,MAAe,EAAA;EAO5C,SAAA,WAAA,EAAA,MAAA;EAQA,SAAA,aAAA,CAAA,EAAA,MAAuB"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//#region src/domain-type-generation.ts
|
|
2
|
+
function serializeValue(value) {
|
|
3
|
+
if (value === null) return "null";
|
|
4
|
+
if (value === void 0) return "undefined";
|
|
5
|
+
if (typeof value === "string") return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
6
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
7
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
8
|
+
if (Array.isArray(value)) return `readonly [${value.map((v) => serializeValue(v)).join(", ")}]`;
|
|
9
|
+
if (typeof value === "object") {
|
|
10
|
+
const entries = [];
|
|
11
|
+
for (const [k, v] of Object.entries(value)) entries.push(`readonly ${serializeObjectKey(k)}: ${serializeValue(v)}`);
|
|
12
|
+
return `{ ${entries.join("; ")} }`;
|
|
13
|
+
}
|
|
14
|
+
return "unknown";
|
|
15
|
+
}
|
|
16
|
+
function serializeObjectKey(key) {
|
|
17
|
+
if (/^[$A-Z_a-z][$\w]*$/.test(key)) return key;
|
|
18
|
+
return serializeValue(key);
|
|
19
|
+
}
|
|
20
|
+
function generateRootsType(roots) {
|
|
21
|
+
if (!roots || Object.keys(roots).length === 0) return "Record<string, string>";
|
|
22
|
+
return `{ ${Object.entries(roots).map(([key, value]) => `readonly ${serializeObjectKey(key)}: ${serializeValue(value)}`).join("; ")} }`;
|
|
23
|
+
}
|
|
24
|
+
function generateModelRelationsType(relations) {
|
|
25
|
+
const relationEntries = [];
|
|
26
|
+
for (const [relName, rel] of Object.entries(relations)) {
|
|
27
|
+
if (typeof rel !== "object" || rel === null) continue;
|
|
28
|
+
const relObj = rel;
|
|
29
|
+
const parts = [];
|
|
30
|
+
if (relObj["to"]) parts.push(`readonly to: ${serializeValue(relObj["to"])}`);
|
|
31
|
+
if (relObj["cardinality"]) parts.push(`readonly cardinality: ${serializeValue(relObj["cardinality"])}`);
|
|
32
|
+
const on = relObj["on"];
|
|
33
|
+
if (on?.localFields && on.targetFields) {
|
|
34
|
+
const localFields = on.localFields.map((f) => serializeValue(f)).join(", ");
|
|
35
|
+
const targetFields = on.targetFields.map((f) => serializeValue(f)).join(", ");
|
|
36
|
+
parts.push(`readonly on: { readonly localFields: readonly [${localFields}]; readonly targetFields: readonly [${targetFields}] }`);
|
|
37
|
+
}
|
|
38
|
+
if (parts.length > 0) relationEntries.push(`readonly ${relName}: { ${parts.join("; ")} }`);
|
|
39
|
+
}
|
|
40
|
+
if (relationEntries.length === 0) return "Record<string, never>";
|
|
41
|
+
return `{ ${relationEntries.join("; ")} }`;
|
|
42
|
+
}
|
|
43
|
+
function deduplicateImports(imports) {
|
|
44
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
45
|
+
const result = [];
|
|
46
|
+
for (const imp of imports) {
|
|
47
|
+
const key = `${imp.package}::${imp.named}`;
|
|
48
|
+
if (!seenKeys.has(key)) {
|
|
49
|
+
seenKeys.add(key);
|
|
50
|
+
result.push(imp);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
function generateImportLines(imports) {
|
|
56
|
+
return imports.map((imp) => {
|
|
57
|
+
return `import type { ${imp.named === imp.alias ? imp.named : `${imp.named} as ${imp.alias}`} } from '${imp.package}';`;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function generateCodecTypeIntersection(imports, named) {
|
|
61
|
+
return imports.filter((imp) => imp.named === named).map((imp) => imp.alias).join(" & ") || "Record<string, never>";
|
|
62
|
+
}
|
|
63
|
+
function generateHashTypeAliases(hashes) {
|
|
64
|
+
const executionHashType = hashes.executionHash ? `ExecutionHashBase<'${hashes.executionHash}'>` : "ExecutionHashBase<string>";
|
|
65
|
+
return [
|
|
66
|
+
`export type StorageHash = StorageHashBase<'${hashes.storageHash}'>;`,
|
|
67
|
+
`export type ExecutionHash = ${executionHashType};`,
|
|
68
|
+
`export type ProfileHash = ProfileHashBase<'${hashes.profileHash}'>;`
|
|
69
|
+
].join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
export { deduplicateImports, generateCodecTypeIntersection, generateHashTypeAliases, generateImportLines, generateModelRelationsType, generateRootsType, serializeObjectKey, serializeValue };
|
|
74
|
+
//# sourceMappingURL=domain-type-generation.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-type-generation.mjs","names":["entries: string[]","relationEntries: string[]","parts: string[]","result: TypesImportSpec[]"],"sources":["../src/domain-type-generation.ts"],"sourcesContent":["import type { TypesImportSpec } from '@prisma-next/contract/types';\n\nexport function serializeValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return 'undefined';\n }\n if (typeof value === 'string') {\n const escaped = value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n return `'${escaped}'`;\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'bigint') {\n return `${value}n`;\n }\n if (Array.isArray(value)) {\n const items = value.map((v) => serializeValue(v)).join(', ');\n return `readonly [${items}]`;\n }\n if (typeof value === 'object') {\n const entries: string[] = [];\n for (const [k, v] of Object.entries(value)) {\n entries.push(`readonly ${serializeObjectKey(k)}: ${serializeValue(v)}`);\n }\n return `{ ${entries.join('; ')} }`;\n }\n return 'unknown';\n}\n\nexport function serializeObjectKey(key: string): string {\n if (/^[$A-Z_a-z][$\\w]*$/.test(key)) {\n return key;\n }\n return serializeValue(key);\n}\n\nexport function generateRootsType(roots: Record<string, string> | undefined): string {\n if (!roots || Object.keys(roots).length === 0) {\n return 'Record<string, string>';\n }\n const entries = Object.entries(roots)\n .map(([key, value]) => `readonly ${serializeObjectKey(key)}: ${serializeValue(value)}`)\n .join('; ');\n return `{ ${entries} }`;\n}\n\nexport function generateModelRelationsType(relations: Record<string, unknown>): string {\n const relationEntries: string[] = [];\n\n for (const [relName, rel] of Object.entries(relations)) {\n if (typeof rel !== 'object' || rel === null) continue;\n const relObj = rel as Record<string, unknown>;\n const parts: string[] = [];\n\n if (relObj['to']) parts.push(`readonly to: ${serializeValue(relObj['to'])}`);\n if (relObj['cardinality'])\n parts.push(`readonly cardinality: ${serializeValue(relObj['cardinality'])}`);\n\n const on = relObj['on'] as { localFields?: string[]; targetFields?: string[] } | undefined;\n if (on?.localFields && on.targetFields) {\n const localFields = on.localFields.map((f) => serializeValue(f)).join(', ');\n const targetFields = on.targetFields.map((f) => serializeValue(f)).join(', ');\n parts.push(\n `readonly on: { readonly localFields: readonly [${localFields}]; readonly targetFields: readonly [${targetFields}] }`,\n );\n }\n\n if (parts.length > 0) {\n relationEntries.push(`readonly ${relName}: { ${parts.join('; ')} }`);\n }\n }\n\n if (relationEntries.length === 0) {\n return 'Record<string, never>';\n }\n\n return `{ ${relationEntries.join('; ')} }`;\n}\n\nexport function deduplicateImports(imports: TypesImportSpec[]): TypesImportSpec[] {\n const seenKeys = new Set<string>();\n const result: TypesImportSpec[] = [];\n for (const imp of imports) {\n const key = `${imp.package}::${imp.named}`;\n if (!seenKeys.has(key)) {\n seenKeys.add(key);\n result.push(imp);\n }\n }\n return result;\n}\n\nexport function generateImportLines(imports: TypesImportSpec[]): string[] {\n return imports.map((imp) => {\n const importClause = imp.named === imp.alias ? imp.named : `${imp.named} as ${imp.alias}`;\n return `import type { ${importClause} } from '${imp.package}';`;\n });\n}\n\nexport function generateCodecTypeIntersection(\n imports: ReadonlyArray<TypesImportSpec>,\n named: string,\n): string {\n const aliases = imports.filter((imp) => imp.named === named).map((imp) => imp.alias);\n return aliases.join(' & ') || 'Record<string, never>';\n}\n\nexport function generateHashTypeAliases(hashes: {\n readonly storageHash: string;\n readonly executionHash?: string;\n readonly profileHash: string;\n}): string {\n const executionHashType = hashes.executionHash\n ? `ExecutionHashBase<'${hashes.executionHash}'>`\n : 'ExecutionHashBase<string>';\n\n return [\n `export type StorageHash = StorageHashBase<'${hashes.storageHash}'>;`,\n `export type ExecutionHash = ${executionHashType};`,\n `export type ProfileHash = ProfileHashBase<'${hashes.profileHash}'>;`,\n ].join('\\n');\n}\n"],"mappings":"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"}
|
package/dist/exports/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deduplicateImports, generateCodecTypeIntersection, generateHashTypeAliases, generateImportLines, generateModelRelationsType, generateRootsType, serializeObjectKey, serializeValue } from "../domain-type-generation.mjs";
|
|
1
2
|
import { EmitOptions, EmitResult, emit } from "@prisma-next/core-control-plane/emission";
|
|
2
3
|
import { TargetFamilyHook, TypesImportSpec, ValidationContext } from "@prisma-next/contract/types";
|
|
3
|
-
export { type EmitOptions, type EmitResult, type TargetFamilyHook, type TypesImportSpec, type ValidationContext, emit };
|
|
4
|
+
export { type EmitOptions, type EmitResult, type TargetFamilyHook, type TypesImportSpec, type ValidationContext, deduplicateImports, emit, generateCodecTypeIntersection, generateHashTypeAliases, generateImportLines, generateModelRelationsType, generateRootsType, serializeObjectKey, serializeValue };
|
package/dist/exports/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deduplicateImports, generateCodecTypeIntersection, generateHashTypeAliases, generateImportLines, generateModelRelationsType, generateRootsType, serializeObjectKey, serializeValue } from "../domain-type-generation.mjs";
|
|
1
2
|
import { emit } from "@prisma-next/core-control-plane/emission";
|
|
2
3
|
|
|
3
|
-
export { emit };
|
|
4
|
+
export { deduplicateImports, emit, generateCodecTypeIntersection, generateHashTypeAliases, generateImportLines, generateModelRelationsType, generateRootsType, serializeObjectKey, serializeValue };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/emitter",
|
|
3
|
-
"version": "0.3.0-dev.
|
|
3
|
+
"version": "0.3.0-dev.129",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"files": [
|
|
@@ -10,24 +10,28 @@
|
|
|
10
10
|
],
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"arktype": "^2.0.0",
|
|
13
|
-
"@prisma-next/contract": "0.3.0-dev.
|
|
14
|
-
"@prisma-next/core-control-plane": "0.3.0-dev.
|
|
13
|
+
"@prisma-next/contract": "0.3.0-dev.129",
|
|
14
|
+
"@prisma-next/core-control-plane": "0.3.0-dev.129"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@types/node": "24.10.4",
|
|
18
18
|
"tsdown": "0.18.4",
|
|
19
19
|
"typescript": "5.9.3",
|
|
20
20
|
"vitest": "4.0.17",
|
|
21
|
-
"@prisma-next/operations": "0.3.0-dev.
|
|
21
|
+
"@prisma-next/operations": "0.3.0-dev.129",
|
|
22
22
|
"@prisma-next/tsdown": "0.0.0",
|
|
23
|
-
"@prisma-next/
|
|
24
|
-
"@prisma-next/
|
|
23
|
+
"@prisma-next/tsconfig": "0.0.0",
|
|
24
|
+
"@prisma-next/test-utils": "0.0.1"
|
|
25
25
|
},
|
|
26
26
|
"exports": {
|
|
27
27
|
".": {
|
|
28
28
|
"types": "./dist/exports/index.d.mts",
|
|
29
29
|
"import": "./dist/exports/index.mjs"
|
|
30
30
|
},
|
|
31
|
+
"./domain-type-generation": {
|
|
32
|
+
"types": "./dist/domain-type-generation.d.mts",
|
|
33
|
+
"import": "./dist/domain-type-generation.mjs"
|
|
34
|
+
},
|
|
31
35
|
"./test/utils": {
|
|
32
36
|
"types": "./dist/test/utils.d.mts",
|
|
33
37
|
"import": "./dist/test/utils.mjs"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { TypesImportSpec } from '@prisma-next/contract/types';
|
|
2
|
+
|
|
3
|
+
export function serializeValue(value: unknown): string {
|
|
4
|
+
if (value === null) {
|
|
5
|
+
return 'null';
|
|
6
|
+
}
|
|
7
|
+
if (value === undefined) {
|
|
8
|
+
return 'undefined';
|
|
9
|
+
}
|
|
10
|
+
if (typeof value === 'string') {
|
|
11
|
+
const escaped = value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
12
|
+
return `'${escaped}'`;
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
15
|
+
return String(value);
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === 'bigint') {
|
|
18
|
+
return `${value}n`;
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
const items = value.map((v) => serializeValue(v)).join(', ');
|
|
22
|
+
return `readonly [${items}]`;
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === 'object') {
|
|
25
|
+
const entries: string[] = [];
|
|
26
|
+
for (const [k, v] of Object.entries(value)) {
|
|
27
|
+
entries.push(`readonly ${serializeObjectKey(k)}: ${serializeValue(v)}`);
|
|
28
|
+
}
|
|
29
|
+
return `{ ${entries.join('; ')} }`;
|
|
30
|
+
}
|
|
31
|
+
return 'unknown';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function serializeObjectKey(key: string): string {
|
|
35
|
+
if (/^[$A-Z_a-z][$\w]*$/.test(key)) {
|
|
36
|
+
return key;
|
|
37
|
+
}
|
|
38
|
+
return serializeValue(key);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function generateRootsType(roots: Record<string, string> | undefined): string {
|
|
42
|
+
if (!roots || Object.keys(roots).length === 0) {
|
|
43
|
+
return 'Record<string, string>';
|
|
44
|
+
}
|
|
45
|
+
const entries = Object.entries(roots)
|
|
46
|
+
.map(([key, value]) => `readonly ${serializeObjectKey(key)}: ${serializeValue(value)}`)
|
|
47
|
+
.join('; ');
|
|
48
|
+
return `{ ${entries} }`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function generateModelRelationsType(relations: Record<string, unknown>): string {
|
|
52
|
+
const relationEntries: string[] = [];
|
|
53
|
+
|
|
54
|
+
for (const [relName, rel] of Object.entries(relations)) {
|
|
55
|
+
if (typeof rel !== 'object' || rel === null) continue;
|
|
56
|
+
const relObj = rel as Record<string, unknown>;
|
|
57
|
+
const parts: string[] = [];
|
|
58
|
+
|
|
59
|
+
if (relObj['to']) parts.push(`readonly to: ${serializeValue(relObj['to'])}`);
|
|
60
|
+
if (relObj['cardinality'])
|
|
61
|
+
parts.push(`readonly cardinality: ${serializeValue(relObj['cardinality'])}`);
|
|
62
|
+
|
|
63
|
+
const on = relObj['on'] as { localFields?: string[]; targetFields?: string[] } | undefined;
|
|
64
|
+
if (on?.localFields && on.targetFields) {
|
|
65
|
+
const localFields = on.localFields.map((f) => serializeValue(f)).join(', ');
|
|
66
|
+
const targetFields = on.targetFields.map((f) => serializeValue(f)).join(', ');
|
|
67
|
+
parts.push(
|
|
68
|
+
`readonly on: { readonly localFields: readonly [${localFields}]; readonly targetFields: readonly [${targetFields}] }`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (parts.length > 0) {
|
|
73
|
+
relationEntries.push(`readonly ${relName}: { ${parts.join('; ')} }`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (relationEntries.length === 0) {
|
|
78
|
+
return 'Record<string, never>';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return `{ ${relationEntries.join('; ')} }`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function deduplicateImports(imports: TypesImportSpec[]): TypesImportSpec[] {
|
|
85
|
+
const seenKeys = new Set<string>();
|
|
86
|
+
const result: TypesImportSpec[] = [];
|
|
87
|
+
for (const imp of imports) {
|
|
88
|
+
const key = `${imp.package}::${imp.named}`;
|
|
89
|
+
if (!seenKeys.has(key)) {
|
|
90
|
+
seenKeys.add(key);
|
|
91
|
+
result.push(imp);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function generateImportLines(imports: TypesImportSpec[]): string[] {
|
|
98
|
+
return imports.map((imp) => {
|
|
99
|
+
const importClause = imp.named === imp.alias ? imp.named : `${imp.named} as ${imp.alias}`;
|
|
100
|
+
return `import type { ${importClause} } from '${imp.package}';`;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function generateCodecTypeIntersection(
|
|
105
|
+
imports: ReadonlyArray<TypesImportSpec>,
|
|
106
|
+
named: string,
|
|
107
|
+
): string {
|
|
108
|
+
const aliases = imports.filter((imp) => imp.named === named).map((imp) => imp.alias);
|
|
109
|
+
return aliases.join(' & ') || 'Record<string, never>';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function generateHashTypeAliases(hashes: {
|
|
113
|
+
readonly storageHash: string;
|
|
114
|
+
readonly executionHash?: string;
|
|
115
|
+
readonly profileHash: string;
|
|
116
|
+
}): string {
|
|
117
|
+
const executionHashType = hashes.executionHash
|
|
118
|
+
? `ExecutionHashBase<'${hashes.executionHash}'>`
|
|
119
|
+
: 'ExecutionHashBase<string>';
|
|
120
|
+
|
|
121
|
+
return [
|
|
122
|
+
`export type StorageHash = StorageHashBase<'${hashes.storageHash}'>;`,
|
|
123
|
+
`export type ExecutionHash = ${executionHashType};`,
|
|
124
|
+
`export type ProfileHash = ProfileHashBase<'${hashes.profileHash}'>;`,
|
|
125
|
+
].join('\n');
|
|
126
|
+
}
|
package/src/exports/index.ts
CHANGED
|
@@ -7,3 +7,13 @@ export type {
|
|
|
7
7
|
export type { EmitOptions, EmitResult } from '@prisma-next/core-control-plane/emission';
|
|
8
8
|
// Re-export emit function and types from core-control-plane
|
|
9
9
|
export { emit } from '@prisma-next/core-control-plane/emission';
|
|
10
|
+
export {
|
|
11
|
+
deduplicateImports,
|
|
12
|
+
generateCodecTypeIntersection,
|
|
13
|
+
generateHashTypeAliases,
|
|
14
|
+
generateImportLines,
|
|
15
|
+
generateModelRelationsType,
|
|
16
|
+
generateRootsType,
|
|
17
|
+
serializeObjectKey,
|
|
18
|
+
serializeValue,
|
|
19
|
+
} from '../domain-type-generation';
|
|
@@ -291,6 +291,67 @@ describe('canonicalization', () => {
|
|
|
291
291
|
expect(columnKeys).toEqual(['a_field', 'm_field', 'z_field']);
|
|
292
292
|
});
|
|
293
293
|
|
|
294
|
+
describe('Mongo storage.collections preservation', () => {
|
|
295
|
+
it('preserves empty storage.collections container', () => {
|
|
296
|
+
const ir = createContractIR({
|
|
297
|
+
targetFamily: 'mongo',
|
|
298
|
+
target: 'mongo',
|
|
299
|
+
storage: { collections: {} },
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const result = canonicalizeContract(ir);
|
|
303
|
+
const parsed = JSON.parse(result) as Record<string, unknown>;
|
|
304
|
+
const storage = parsed['storage'] as Record<string, unknown>;
|
|
305
|
+
expect(storage['collections']).toEqual({});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('preserves collection entries with empty payloads', () => {
|
|
309
|
+
const ir = createContractIR({
|
|
310
|
+
targetFamily: 'mongo',
|
|
311
|
+
target: 'mongo',
|
|
312
|
+
storage: { collections: { users: {}, posts: {} } },
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const result = canonicalizeContract(ir);
|
|
316
|
+
const parsed = JSON.parse(result) as Record<string, unknown>;
|
|
317
|
+
const storage = parsed['storage'] as Record<string, unknown>;
|
|
318
|
+
const collections = storage['collections'] as Record<string, unknown>;
|
|
319
|
+
expect(collections['users']).toEqual({});
|
|
320
|
+
expect(collections['posts']).toEqual({});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('sorts collection names lexicographically', () => {
|
|
324
|
+
const ir = createContractIR({
|
|
325
|
+
targetFamily: 'mongo',
|
|
326
|
+
target: 'mongo',
|
|
327
|
+
storage: { collections: { zebras: {}, apples: {}, mangoes: {} } },
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const result = canonicalizeContract(ir);
|
|
331
|
+
const parsed = JSON.parse(result) as Record<string, unknown>;
|
|
332
|
+
const storage = parsed['storage'] as Record<string, unknown>;
|
|
333
|
+
const collections = storage['collections'] as Record<string, unknown>;
|
|
334
|
+
expect(Object.keys(collections)).toEqual(['apples', 'mangoes', 'zebras']);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('produces different hashes when collections differ', () => {
|
|
338
|
+
const ir1 = createContractIR({
|
|
339
|
+
targetFamily: 'mongo',
|
|
340
|
+
target: 'mongo',
|
|
341
|
+
storage: { collections: { users: {} } },
|
|
342
|
+
});
|
|
343
|
+
const ir2 = createContractIR({
|
|
344
|
+
targetFamily: 'mongo',
|
|
345
|
+
target: 'mongo',
|
|
346
|
+
storage: { collections: { users: {}, posts: {} } },
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const result1 = canonicalizeContract(ir1);
|
|
350
|
+
const result2 = canonicalizeContract(ir2);
|
|
351
|
+
expect(result1).not.toBe(result2);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
294
355
|
it('sorts extension namespaces lexicographically', () => {
|
|
295
356
|
const ir = createContractIR({
|
|
296
357
|
extensionPacks: {
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type { TypesImportSpec } from '@prisma-next/contract/types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
deduplicateImports,
|
|
5
|
+
generateCodecTypeIntersection,
|
|
6
|
+
generateHashTypeAliases,
|
|
7
|
+
generateImportLines,
|
|
8
|
+
generateModelRelationsType,
|
|
9
|
+
generateRootsType,
|
|
10
|
+
serializeObjectKey,
|
|
11
|
+
serializeValue,
|
|
12
|
+
} from '../src/domain-type-generation';
|
|
13
|
+
|
|
14
|
+
describe('serializeValue', () => {
|
|
15
|
+
it('serializes null', () => {
|
|
16
|
+
expect(serializeValue(null)).toBe('null');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('serializes undefined', () => {
|
|
20
|
+
expect(serializeValue(undefined)).toBe('undefined');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('serializes strings with single quotes', () => {
|
|
24
|
+
expect(serializeValue('hello')).toBe("'hello'");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('escapes backslashes and single quotes in strings', () => {
|
|
28
|
+
expect(serializeValue("it's")).toBe("'it\\'s'");
|
|
29
|
+
expect(serializeValue('back\\slash')).toBe("'back\\\\slash'");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('serializes numbers', () => {
|
|
33
|
+
expect(serializeValue(42)).toBe('42');
|
|
34
|
+
expect(serializeValue(3.14)).toBe('3.14');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('serializes booleans', () => {
|
|
38
|
+
expect(serializeValue(true)).toBe('true');
|
|
39
|
+
expect(serializeValue(false)).toBe('false');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('serializes bigints', () => {
|
|
43
|
+
expect(serializeValue(BigInt(123))).toBe('123n');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('serializes arrays as readonly tuples', () => {
|
|
47
|
+
expect(serializeValue(['a', 'b'])).toBe("readonly ['a', 'b']");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('serializes objects with readonly properties', () => {
|
|
51
|
+
expect(serializeValue({ key: 'val' })).toBe("{ readonly key: 'val' }");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('serializes nested objects', () => {
|
|
55
|
+
const result = serializeValue({ a: { b: 1 } });
|
|
56
|
+
expect(result).toBe('{ readonly a: { readonly b: 1 } }');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('returns unknown for unsupported types', () => {
|
|
60
|
+
expect(serializeValue(Symbol('test'))).toBe('unknown');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('serializeObjectKey', () => {
|
|
65
|
+
it('passes through valid identifiers', () => {
|
|
66
|
+
expect(serializeObjectKey('foo')).toBe('foo');
|
|
67
|
+
expect(serializeObjectKey('_bar')).toBe('_bar');
|
|
68
|
+
expect(serializeObjectKey('$baz')).toBe('$baz');
|
|
69
|
+
expect(serializeObjectKey('camelCase')).toBe('camelCase');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('quotes keys with special characters', () => {
|
|
73
|
+
expect(serializeObjectKey('has space')).toBe("'has space'");
|
|
74
|
+
expect(serializeObjectKey('has-dash')).toBe("'has-dash'");
|
|
75
|
+
expect(serializeObjectKey('ns/name@1')).toBe("'ns/name@1'");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('generateRootsType', () => {
|
|
80
|
+
it('returns Record<string, string> for undefined roots', () => {
|
|
81
|
+
expect(generateRootsType(undefined)).toBe('Record<string, string>');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('returns Record<string, string> for empty roots', () => {
|
|
85
|
+
expect(generateRootsType({})).toBe('Record<string, string>');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('generates literal object type for roots', () => {
|
|
89
|
+
const result = generateRootsType({ users: 'User', posts: 'Post' });
|
|
90
|
+
expect(result).toContain("readonly users: 'User'");
|
|
91
|
+
expect(result).toContain("readonly posts: 'Post'");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('generateModelRelationsType', () => {
|
|
96
|
+
it('returns empty object for empty relations', () => {
|
|
97
|
+
expect(generateModelRelationsType({})).toBe('Record<string, never>');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('generates relation with to and cardinality', () => {
|
|
101
|
+
const result = generateModelRelationsType({
|
|
102
|
+
posts: { to: 'Post', cardinality: '1:N' },
|
|
103
|
+
});
|
|
104
|
+
expect(result).toContain("readonly to: 'Post'");
|
|
105
|
+
expect(result).toContain("readonly cardinality: '1:N'");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('generates relation with on (localFields/targetFields)', () => {
|
|
109
|
+
const result = generateModelRelationsType({
|
|
110
|
+
author: {
|
|
111
|
+
to: 'User',
|
|
112
|
+
cardinality: 'N:1',
|
|
113
|
+
on: { localFields: ['authorId'], targetFields: ['_id'] },
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
expect(result).toContain("readonly to: 'User'");
|
|
117
|
+
expect(result).toContain("readonly cardinality: 'N:1'");
|
|
118
|
+
expect(result).toContain("readonly localFields: readonly ['authorId']");
|
|
119
|
+
expect(result).toContain("readonly targetFields: readonly ['_id']");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('skips non-object relations', () => {
|
|
123
|
+
const result = generateModelRelationsType({
|
|
124
|
+
bad: 'not an object' as unknown as Record<string, unknown>,
|
|
125
|
+
});
|
|
126
|
+
expect(result).toBe('Record<string, never>');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('generates multiple relations', () => {
|
|
130
|
+
const result = generateModelRelationsType({
|
|
131
|
+
author: { to: 'User', cardinality: 'N:1' },
|
|
132
|
+
comments: { to: 'Comment', cardinality: '1:N' },
|
|
133
|
+
});
|
|
134
|
+
expect(result).toContain('readonly author:');
|
|
135
|
+
expect(result).toContain('readonly comments:');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('omits to when missing from relation', () => {
|
|
139
|
+
const result = generateModelRelationsType({
|
|
140
|
+
rel: { cardinality: '1:N' },
|
|
141
|
+
});
|
|
142
|
+
expect(result).toContain("readonly cardinality: '1:N'");
|
|
143
|
+
expect(result).not.toContain('readonly to:');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('omits cardinality when missing from relation', () => {
|
|
147
|
+
const result = generateModelRelationsType({
|
|
148
|
+
rel: { to: 'Post' },
|
|
149
|
+
});
|
|
150
|
+
expect(result).toContain("readonly to: 'Post'");
|
|
151
|
+
expect(result).not.toContain('readonly cardinality:');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('skips relation object with no recognized properties', () => {
|
|
155
|
+
const result = generateModelRelationsType({
|
|
156
|
+
empty: { unknown: true },
|
|
157
|
+
});
|
|
158
|
+
expect(result).toBe('Record<string, never>');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('deduplicateImports', () => {
|
|
163
|
+
it('returns empty array for empty input', () => {
|
|
164
|
+
expect(deduplicateImports([])).toEqual([]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('keeps unique imports', () => {
|
|
168
|
+
const imports: TypesImportSpec[] = [
|
|
169
|
+
{ package: 'pkg-a', named: 'CodecTypes', alias: 'A' },
|
|
170
|
+
{ package: 'pkg-b', named: 'CodecTypes', alias: 'B' },
|
|
171
|
+
];
|
|
172
|
+
expect(deduplicateImports(imports)).toHaveLength(2);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('deduplicates by package+named (first wins)', () => {
|
|
176
|
+
const imports: TypesImportSpec[] = [
|
|
177
|
+
{ package: 'pkg-a', named: 'CodecTypes', alias: 'First' },
|
|
178
|
+
{ package: 'pkg-a', named: 'CodecTypes', alias: 'Second' },
|
|
179
|
+
];
|
|
180
|
+
const result = deduplicateImports(imports);
|
|
181
|
+
expect(result).toHaveLength(1);
|
|
182
|
+
expect(result[0]!.alias).toBe('First');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('preserves insertion order', () => {
|
|
186
|
+
const imports: TypesImportSpec[] = [
|
|
187
|
+
{ package: 'pkg-b', named: 'X', alias: 'X' },
|
|
188
|
+
{ package: 'pkg-a', named: 'Y', alias: 'Y' },
|
|
189
|
+
];
|
|
190
|
+
const result = deduplicateImports(imports);
|
|
191
|
+
expect(result[0]!.package).toBe('pkg-b');
|
|
192
|
+
expect(result[1]!.package).toBe('pkg-a');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('generateImportLines', () => {
|
|
197
|
+
it('generates import with alias', () => {
|
|
198
|
+
const imports: TypesImportSpec[] = [
|
|
199
|
+
{ package: '@prisma-next/adapter', named: 'CodecTypes', alias: 'PgCodecTypes' },
|
|
200
|
+
];
|
|
201
|
+
const lines = generateImportLines(imports);
|
|
202
|
+
expect(lines).toEqual([
|
|
203
|
+
"import type { CodecTypes as PgCodecTypes } from '@prisma-next/adapter';",
|
|
204
|
+
]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('simplifies import when named === alias', () => {
|
|
208
|
+
const imports: TypesImportSpec[] = [
|
|
209
|
+
{ package: '@prisma-next/adapter', named: 'Vector', alias: 'Vector' },
|
|
210
|
+
];
|
|
211
|
+
const lines = generateImportLines(imports);
|
|
212
|
+
expect(lines).toEqual(["import type { Vector } from '@prisma-next/adapter';"]);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('generateCodecTypeIntersection', () => {
|
|
217
|
+
it('returns Record<string, never> when no matching imports', () => {
|
|
218
|
+
expect(generateCodecTypeIntersection([], 'CodecTypes')).toBe('Record<string, never>');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('returns single alias when one match', () => {
|
|
222
|
+
const imports: TypesImportSpec[] = [
|
|
223
|
+
{ package: 'pkg', named: 'CodecTypes', alias: 'PgCodecTypes' },
|
|
224
|
+
];
|
|
225
|
+
expect(generateCodecTypeIntersection(imports, 'CodecTypes')).toBe('PgCodecTypes');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('returns intersection when multiple matches', () => {
|
|
229
|
+
const imports: TypesImportSpec[] = [
|
|
230
|
+
{ package: 'pkg-a', named: 'CodecTypes', alias: 'A' },
|
|
231
|
+
{ package: 'pkg-b', named: 'CodecTypes', alias: 'B' },
|
|
232
|
+
];
|
|
233
|
+
expect(generateCodecTypeIntersection(imports, 'CodecTypes')).toBe('A & B');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('filters by named parameter', () => {
|
|
237
|
+
const imports: TypesImportSpec[] = [
|
|
238
|
+
{ package: 'pkg', named: 'CodecTypes', alias: 'CT' },
|
|
239
|
+
{ package: 'pkg', named: 'OperationTypes', alias: 'OT' },
|
|
240
|
+
];
|
|
241
|
+
expect(generateCodecTypeIntersection(imports, 'OperationTypes')).toBe('OT');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('generateHashTypeAliases', () => {
|
|
246
|
+
it('generates storage and profile hash aliases', () => {
|
|
247
|
+
const result = generateHashTypeAliases({
|
|
248
|
+
storageHash: 'sha256:abc123',
|
|
249
|
+
profileHash: 'sha256:def456',
|
|
250
|
+
});
|
|
251
|
+
expect(result).toContain("StorageHashBase<'sha256:abc123'>");
|
|
252
|
+
expect(result).toContain("ProfileHashBase<'sha256:def456'>");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('generates concrete execution hash when provided', () => {
|
|
256
|
+
const result = generateHashTypeAliases({
|
|
257
|
+
storageHash: 'sha256:abc',
|
|
258
|
+
executionHash: 'sha256:exec',
|
|
259
|
+
profileHash: 'sha256:prof',
|
|
260
|
+
});
|
|
261
|
+
expect(result).toContain("ExecutionHashBase<'sha256:exec'>");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('generates generic execution hash when not provided', () => {
|
|
265
|
+
const result = generateHashTypeAliases({
|
|
266
|
+
storageHash: 'sha256:abc',
|
|
267
|
+
profileHash: 'sha256:prof',
|
|
268
|
+
});
|
|
269
|
+
expect(result).toContain('ExecutionHashBase<string>');
|
|
270
|
+
});
|
|
271
|
+
});
|