@prisma-next/emitter 0.3.0-dev.3 → 0.3.0-dev.30
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/src/exports/index.d.ts +4 -0
- package/dist/src/exports/index.d.ts.map +1 -0
- package/dist/src/target-family.d.ts +2 -0
- package/dist/src/target-family.d.ts.map +1 -0
- package/dist/test/utils.d.ts +3 -5
- package/dist/test/utils.d.ts.map +1 -0
- package/package.json +17 -15
- package/src/exports/index.ts +9 -0
- package/src/target-family.ts +7 -0
- package/test/canonicalization.test.ts +210 -0
- package/test/emitter.integration.test.ts +274 -0
- package/test/emitter.roundtrip.test.ts +370 -0
- package/test/emitter.test.ts +703 -0
- package/test/factories.test.ts +274 -0
- package/test/hashing.test.ts +59 -0
- package/test/utils.ts +166 -0
- package/dist/exports/index.d.ts +0 -2
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { TargetFamilyHook, TypesImportSpec, ValidationContext, } from '@prisma-next/contract/types';
|
|
2
|
+
export type { EmitOptions, EmitResult } from '@prisma-next/core-control-plane/emission';
|
|
3
|
+
export { emit } from '@prisma-next/core-control-plane/emission';
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/exports/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,iBAAiB,GAClB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAC;AAExF,OAAO,EAAE,IAAI,EAAE,MAAM,0CAA0C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"target-family.d.ts","sourceRoot":"","sources":["../../src/target-family.ts"],"names":[],"mappings":"AAEA,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,iBAAiB,GAClB,MAAM,6BAA6B,CAAC"}
|
package/dist/test/utils.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { ContractIR } from '@prisma-next/contract/ir';
|
|
2
|
-
|
|
1
|
+
import { type ContractIR } from '@prisma-next/contract/ir';
|
|
3
2
|
/**
|
|
4
3
|
* Factory function for creating ContractIR objects in tests.
|
|
5
4
|
* Provides sensible defaults and allows overriding specific fields.
|
|
@@ -8,9 +7,8 @@ import { ContractIR } from '@prisma-next/contract/ir';
|
|
|
8
7
|
* If a field is explicitly set to `undefined` in overrides, it will be omitted
|
|
9
8
|
* from the result (useful for testing validation of missing fields).
|
|
10
9
|
*/
|
|
11
|
-
declare function createContractIR(overrides?: Partial<ContractIR> & {
|
|
10
|
+
export declare function createContractIR(overrides?: Partial<ContractIR> & {
|
|
12
11
|
coreHash?: string;
|
|
13
12
|
profileHash?: string;
|
|
14
13
|
}): ContractIR;
|
|
15
|
-
|
|
16
|
-
export { createContractIR };
|
|
14
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../test/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAoB,MAAM,0BAA0B,CAAC;AAE7E;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,GAAE,OAAO,CAAC,UAAU,CAAC,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GAChF,UAAU,CAyJZ"}
|
package/package.json
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/emitter",
|
|
3
|
-
"version": "0.3.0-dev.
|
|
3
|
+
"version": "0.3.0-dev.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"files": [
|
|
7
|
-
"dist"
|
|
7
|
+
"dist",
|
|
8
|
+
"src",
|
|
9
|
+
"test"
|
|
8
10
|
],
|
|
9
11
|
"dependencies": {
|
|
10
12
|
"arktype": "^2.0.0",
|
|
11
|
-
"@prisma-next/contract": "0.3.0-dev.
|
|
12
|
-
"@prisma-next/core-control-plane": "0.3.0-dev.
|
|
13
|
+
"@prisma-next/contract": "0.3.0-dev.30",
|
|
14
|
+
"@prisma-next/core-control-plane": "0.3.0-dev.30"
|
|
13
15
|
},
|
|
14
16
|
"devDependencies": {
|
|
15
17
|
"@types/node": "24.10.4",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"@prisma-next/
|
|
18
|
+
"tsup": "8.5.1",
|
|
19
|
+
"typescript": "5.9.3",
|
|
20
|
+
"vitest": "4.0.16",
|
|
21
|
+
"@prisma-next/operations": "0.3.0-dev.30",
|
|
22
|
+
"@prisma-next/tsconfig": "0.0.0",
|
|
21
23
|
"@prisma-next/test-utils": "0.0.1"
|
|
22
24
|
},
|
|
23
25
|
"exports": {
|
|
24
26
|
".": {
|
|
25
|
-
"types": "./dist/exports/index.d.ts",
|
|
27
|
+
"types": "./dist/src/exports/index.d.ts",
|
|
26
28
|
"import": "./dist/exports/index.js"
|
|
27
29
|
},
|
|
28
30
|
"./test/utils": {
|
|
@@ -31,13 +33,13 @@
|
|
|
31
33
|
}
|
|
32
34
|
},
|
|
33
35
|
"scripts": {
|
|
34
|
-
"build": "tsup --config tsup.config.ts",
|
|
36
|
+
"build": "tsup --config tsup.config.ts && tsc --project tsconfig.build.json",
|
|
35
37
|
"test": "vitest run",
|
|
36
38
|
"test:coverage": "vitest run --coverage",
|
|
37
39
|
"typecheck": "tsc --project tsconfig.json --noEmit",
|
|
38
|
-
"lint": "biome check . --
|
|
39
|
-
"lint:fix": "biome check --write .
|
|
40
|
-
"lint:fix:unsafe": "biome check --write --unsafe .
|
|
41
|
-
"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"
|
|
42
44
|
}
|
|
43
45
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Re-export types from @prisma-next/contract for backward compatibility
|
|
2
|
+
export type {
|
|
3
|
+
TargetFamilyHook,
|
|
4
|
+
TypesImportSpec,
|
|
5
|
+
ValidationContext,
|
|
6
|
+
} from '@prisma-next/contract/types';
|
|
7
|
+
export type { EmitOptions, EmitResult } from '@prisma-next/core-control-plane/emission';
|
|
8
|
+
// Re-export emit function and types from core-control-plane
|
|
9
|
+
export { emit } from '@prisma-next/core-control-plane/emission';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Re-export types from @prisma-next/contract for backward compatibility
|
|
2
|
+
// These types were moved to @prisma-next/contract to resolve dependency violations
|
|
3
|
+
export type {
|
|
4
|
+
TargetFamilyHook,
|
|
5
|
+
TypesImportSpec,
|
|
6
|
+
ValidationContext,
|
|
7
|
+
} from '@prisma-next/contract/types';
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { canonicalizeContract } from '@prisma-next/core-control-plane/emission';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { createContractIR } from './utils';
|
|
4
|
+
|
|
5
|
+
describe('canonicalization', () => {
|
|
6
|
+
it('orders top-level sections correctly', () => {
|
|
7
|
+
const ir = createContractIR({
|
|
8
|
+
capabilities: { postgres: { jsonAgg: true } },
|
|
9
|
+
meta: { source: 'test' },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const result = canonicalizeContract(ir);
|
|
13
|
+
const parsed = JSON.parse(result) as Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
const keys = Object.keys(parsed);
|
|
16
|
+
const schemaVersionIndex = keys.indexOf('schemaVersion');
|
|
17
|
+
const targetFamilyIndex = keys.indexOf('targetFamily');
|
|
18
|
+
const targetIndex = keys.indexOf('target');
|
|
19
|
+
const modelsIndex = keys.indexOf('models');
|
|
20
|
+
const storageIndex = keys.indexOf('storage');
|
|
21
|
+
const capabilitiesIndex = keys.indexOf('capabilities');
|
|
22
|
+
const metaIndex = keys.indexOf('meta');
|
|
23
|
+
|
|
24
|
+
expect(schemaVersionIndex).toBeLessThan(targetFamilyIndex);
|
|
25
|
+
expect(targetFamilyIndex).toBeLessThan(targetIndex);
|
|
26
|
+
expect(targetIndex).toBeLessThan(modelsIndex);
|
|
27
|
+
expect(modelsIndex).toBeLessThan(storageIndex);
|
|
28
|
+
expect(storageIndex).toBeLessThan(capabilitiesIndex);
|
|
29
|
+
expect(capabilitiesIndex).toBeLessThan(metaIndex);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('omits nullable false from columns', () => {
|
|
33
|
+
const ir = createContractIR({
|
|
34
|
+
storage: {
|
|
35
|
+
tables: {
|
|
36
|
+
user: {
|
|
37
|
+
columns: {
|
|
38
|
+
id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
|
|
39
|
+
email: { codecId: 'pg/text@1', nativeType: 'text', nullable: true },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const result = canonicalizeContract(ir);
|
|
47
|
+
const parsed = JSON.parse(result) as Record<string, unknown>;
|
|
48
|
+
const storage = parsed['storage'] as Record<string, unknown>;
|
|
49
|
+
const tables = storage['tables'] as Record<string, unknown>;
|
|
50
|
+
const user = tables['user'] as Record<string, unknown>;
|
|
51
|
+
const columns = user['columns'] as Record<string, unknown>;
|
|
52
|
+
const id = columns['id'] as Record<string, unknown>;
|
|
53
|
+
const email = columns['email'] as Record<string, unknown>;
|
|
54
|
+
expect(id['nullable']).toBeUndefined();
|
|
55
|
+
expect(email['nullable']).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('omits empty arrays and objects except required ones', () => {
|
|
59
|
+
const ir = createContractIR();
|
|
60
|
+
|
|
61
|
+
const result = canonicalizeContract(ir);
|
|
62
|
+
const parsed = JSON.parse(result);
|
|
63
|
+
expect(parsed).toMatchObject({
|
|
64
|
+
models: expect.anything(),
|
|
65
|
+
storage: {
|
|
66
|
+
tables: expect.anything(),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
// Required top-level fields (capabilities, extensionPacks, meta, relations, sources) are preserved even when empty
|
|
70
|
+
// because they are required by ContractIR and needed for round-trip tests
|
|
71
|
+
expect(parsed).toMatchObject({
|
|
72
|
+
capabilities: expect.anything(),
|
|
73
|
+
extensionPacks: expect.anything(),
|
|
74
|
+
meta: expect.anything(),
|
|
75
|
+
relations: expect.anything(),
|
|
76
|
+
sources: expect.anything(),
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('preserves semantic array order for column lists', () => {
|
|
81
|
+
const ir = createContractIR({
|
|
82
|
+
storage: {
|
|
83
|
+
tables: {
|
|
84
|
+
user: {
|
|
85
|
+
columns: {
|
|
86
|
+
first: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
|
|
87
|
+
second: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
|
|
88
|
+
},
|
|
89
|
+
primaryKey: {
|
|
90
|
+
columns: ['second', 'first'],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const result1 = canonicalizeContract(ir);
|
|
98
|
+
|
|
99
|
+
const ir2 = createContractIR({
|
|
100
|
+
storage: {
|
|
101
|
+
tables: {
|
|
102
|
+
user: {
|
|
103
|
+
columns: {
|
|
104
|
+
first: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
|
|
105
|
+
second: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
|
|
106
|
+
},
|
|
107
|
+
primaryKey: {
|
|
108
|
+
columns: ['first', 'second'],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const result2 = canonicalizeContract(ir2);
|
|
116
|
+
|
|
117
|
+
expect(result1).not.toBe(result2);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('sorts non-semantic arrays by canonical name', () => {
|
|
121
|
+
const ir = createContractIR({
|
|
122
|
+
storage: {
|
|
123
|
+
tables: {
|
|
124
|
+
user: {
|
|
125
|
+
columns: {
|
|
126
|
+
id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
|
|
127
|
+
},
|
|
128
|
+
indexes: [
|
|
129
|
+
{ columns: ['id'], name: 'user_email_idx' },
|
|
130
|
+
{ columns: ['id'], name: 'user_name_idx' },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const result = canonicalizeContract(ir);
|
|
138
|
+
const parsed = JSON.parse(result) as Record<string, unknown>;
|
|
139
|
+
const storage = parsed['storage'] as Record<string, unknown>;
|
|
140
|
+
const tables = storage['tables'] as Record<string, unknown>;
|
|
141
|
+
const user = tables['user'] as Record<string, unknown>;
|
|
142
|
+
const indexes = user['indexes'] as Array<{ name: string }>;
|
|
143
|
+
const indexNames = indexes.map((idx) => idx.name);
|
|
144
|
+
expect(indexNames).toEqual(['user_email_idx', 'user_name_idx']);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('sorts nested object keys lexicographically', () => {
|
|
148
|
+
const ir = createContractIR({
|
|
149
|
+
storage: {
|
|
150
|
+
tables: {
|
|
151
|
+
user: {
|
|
152
|
+
columns: {
|
|
153
|
+
z_field: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
|
|
154
|
+
a_field: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
|
|
155
|
+
m_field: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const result = canonicalizeContract(ir);
|
|
163
|
+
const parsed = JSON.parse(result) as Record<string, unknown>;
|
|
164
|
+
const storage = parsed['storage'] as Record<string, unknown>;
|
|
165
|
+
const tables = storage['tables'] as Record<string, unknown>;
|
|
166
|
+
const user = tables['user'] as Record<string, unknown>;
|
|
167
|
+
const columns = user['columns'] as Record<string, unknown>;
|
|
168
|
+
const columnKeys = Object.keys(columns);
|
|
169
|
+
expect(columnKeys).toEqual(['a_field', 'm_field', 'z_field']);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('sorts extension namespaces lexicographically', () => {
|
|
173
|
+
const ir = createContractIR({
|
|
174
|
+
extensionPacks: {
|
|
175
|
+
pgvector: { version: '0.0.1' },
|
|
176
|
+
postgres: { version: '0.0.1' },
|
|
177
|
+
another: { version: '0.0.1' },
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const result = canonicalizeContract(ir);
|
|
182
|
+
const parsed = JSON.parse(result) as Record<string, unknown>;
|
|
183
|
+
const extensionPacks = parsed['extensionPacks'] as Record<string, unknown>;
|
|
184
|
+
const extensionKeys = Object.keys(extensionPacks);
|
|
185
|
+
expect(extensionKeys).toEqual(['another', 'pgvector', 'postgres']);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('omits generated false', () => {
|
|
189
|
+
const ir = createContractIR({
|
|
190
|
+
storage: {
|
|
191
|
+
tables: {
|
|
192
|
+
user: {
|
|
193
|
+
columns: {
|
|
194
|
+
id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false, generated: false },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const result = canonicalizeContract(ir);
|
|
202
|
+
const parsed = JSON.parse(result) as Record<string, unknown>;
|
|
203
|
+
const storage = parsed['storage'] as Record<string, unknown>;
|
|
204
|
+
const tables = storage['tables'] as Record<string, unknown>;
|
|
205
|
+
const user = tables['user'] as Record<string, unknown>;
|
|
206
|
+
const columns = user['columns'] as Record<string, unknown>;
|
|
207
|
+
const id = columns['id'] as Record<string, unknown>;
|
|
208
|
+
expect(id['generated']).toBeUndefined();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import type { ContractIR } from '@prisma-next/contract/ir';
|
|
2
|
+
import type {
|
|
3
|
+
TargetFamilyHook,
|
|
4
|
+
TypesImportSpec,
|
|
5
|
+
ValidationContext,
|
|
6
|
+
} from '@prisma-next/contract/types';
|
|
7
|
+
import type { EmitOptions } from '@prisma-next/core-control-plane/emission';
|
|
8
|
+
import { emit } from '@prisma-next/core-control-plane/emission';
|
|
9
|
+
import { createOperationRegistry } from '@prisma-next/operations';
|
|
10
|
+
import { timeouts } from '@prisma-next/test-utils';
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
import { createContractIR } from './utils';
|
|
13
|
+
|
|
14
|
+
const mockSqlHook: TargetFamilyHook = {
|
|
15
|
+
id: 'sql',
|
|
16
|
+
validateTypes: (ir: ContractIR, _ctx: ValidationContext) => {
|
|
17
|
+
const storage = ir.storage as
|
|
18
|
+
| { tables?: Record<string, { columns?: Record<string, { type?: string }> }> }
|
|
19
|
+
| undefined;
|
|
20
|
+
if (!storage?.tables) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const referencedNamespaces = new Set<string>();
|
|
25
|
+
const extensionPacks = ir.extensionPacks as Record<string, unknown> | undefined;
|
|
26
|
+
if (extensionPacks) {
|
|
27
|
+
for (const namespace of Object.keys(extensionPacks)) {
|
|
28
|
+
referencedNamespaces.add(namespace);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const typeIdRegex = /^([^/]+)\/([^@]+)@(\d+)$/;
|
|
33
|
+
|
|
34
|
+
for (const [tableName, table] of Object.entries(storage.tables)) {
|
|
35
|
+
if (!table.columns) continue;
|
|
36
|
+
for (const [colName, col] of Object.entries(table.columns)) {
|
|
37
|
+
const column = col as { codecId?: string };
|
|
38
|
+
if (!column.codecId) {
|
|
39
|
+
throw new Error(`Column "${colName}" in table "${tableName}" is missing codecId`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!typeIdRegex.test(column.codecId)) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Column "${colName}" in table "${tableName}" has invalid codecId format "${column.codecId}". Expected format: ns/name@version`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const match = column.codecId.match(typeIdRegex);
|
|
49
|
+
if (match?.[1]) {
|
|
50
|
+
const namespace = match[1];
|
|
51
|
+
if (!referencedNamespaces.has(namespace)) {
|
|
52
|
+
if (namespace === 'pg' && referencedNamespaces.has('postgres')) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Column "${colName}" in table "${tableName}" uses codecId "${column.codecId}" from namespace "${namespace}" which is not referenced in contract.extensionPacks`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
validateStructure: (ir: ContractIR) => {
|
|
64
|
+
if (ir.targetFamily !== 'sql') {
|
|
65
|
+
throw new Error(`Expected targetFamily "sql", got "${ir.targetFamily}"`);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
generateContractTypes: (_ir, _codecTypeImports, _operationTypeImports) => {
|
|
69
|
+
void _codecTypeImports;
|
|
70
|
+
void _operationTypeImports;
|
|
71
|
+
return `// Generated contract types
|
|
72
|
+
export type CodecTypes = Record<string, never>;
|
|
73
|
+
export type LaneCodecTypes = CodecTypes;
|
|
74
|
+
export type Contract = unknown;
|
|
75
|
+
`;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
describe('emitter integration', () => {
|
|
80
|
+
it(
|
|
81
|
+
'emits complete contract from IR to artifacts',
|
|
82
|
+
async () => {
|
|
83
|
+
const ir = createContractIR({
|
|
84
|
+
models: {
|
|
85
|
+
User: {
|
|
86
|
+
storage: { table: 'user' },
|
|
87
|
+
fields: {
|
|
88
|
+
id: { column: 'id' },
|
|
89
|
+
email: { column: 'email' },
|
|
90
|
+
},
|
|
91
|
+
relations: {},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
storage: {
|
|
95
|
+
tables: {
|
|
96
|
+
user: {
|
|
97
|
+
columns: {
|
|
98
|
+
id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
|
|
99
|
+
email: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
|
|
100
|
+
},
|
|
101
|
+
primaryKey: { columns: ['id'] },
|
|
102
|
+
uniques: [],
|
|
103
|
+
indexes: [],
|
|
104
|
+
foreignKeys: [],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
extensionPacks: {
|
|
109
|
+
postgres: {
|
|
110
|
+
version: '0.0.1',
|
|
111
|
+
},
|
|
112
|
+
pg: {},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Create minimal test data (emitter tests don't load packs)
|
|
117
|
+
const operationRegistry = createOperationRegistry();
|
|
118
|
+
const codecTypeImports: TypesImportSpec[] = [];
|
|
119
|
+
const operationTypeImports: TypesImportSpec[] = [];
|
|
120
|
+
const extensionIds = ['postgres', 'pg'];
|
|
121
|
+
const options: EmitOptions = {
|
|
122
|
+
outputDir: '',
|
|
123
|
+
operationRegistry,
|
|
124
|
+
codecTypeImports,
|
|
125
|
+
operationTypeImports,
|
|
126
|
+
extensionIds,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const result = await emit(ir, options, mockSqlHook);
|
|
130
|
+
|
|
131
|
+
expect(result.coreHash).toMatch(/^sha256:[a-f0-9]{64}$/);
|
|
132
|
+
expect(result.contractDts).toContain('export type Contract');
|
|
133
|
+
expect(result.contractDts).toContain('CodecTypes');
|
|
134
|
+
expect(result.contractDts).toContain('LaneCodecTypes');
|
|
135
|
+
|
|
136
|
+
const contractJson = JSON.parse(result.contractJson);
|
|
137
|
+
expect(contractJson).toMatchObject({
|
|
138
|
+
schemaVersion: '1',
|
|
139
|
+
targetFamily: 'sql',
|
|
140
|
+
target: 'postgres',
|
|
141
|
+
coreHash: result.coreHash,
|
|
142
|
+
storage: {
|
|
143
|
+
tables: {
|
|
144
|
+
user: expect.anything(),
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
timeouts.typeScriptCompilation,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
it('produces stable hashes for identical input', async () => {
|
|
153
|
+
const ir = createContractIR({
|
|
154
|
+
models: {
|
|
155
|
+
User: {
|
|
156
|
+
storage: { table: 'user' },
|
|
157
|
+
fields: {
|
|
158
|
+
id: { column: 'id' },
|
|
159
|
+
},
|
|
160
|
+
relations: {},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
storage: {
|
|
164
|
+
tables: {
|
|
165
|
+
user: {
|
|
166
|
+
columns: {
|
|
167
|
+
id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
|
|
168
|
+
},
|
|
169
|
+
primaryKey: { columns: ['id'] },
|
|
170
|
+
uniques: [],
|
|
171
|
+
indexes: [],
|
|
172
|
+
foreignKeys: [],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
extensionPacks: {
|
|
177
|
+
postgres: {
|
|
178
|
+
version: '0.0.1',
|
|
179
|
+
},
|
|
180
|
+
pg: {},
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Create minimal test data (emitter tests don't load packs)
|
|
185
|
+
const operationRegistry = createOperationRegistry();
|
|
186
|
+
const codecTypeImports: TypesImportSpec[] = [];
|
|
187
|
+
const operationTypeImports: TypesImportSpec[] = [];
|
|
188
|
+
const extensionIds = ['postgres', 'pg'];
|
|
189
|
+
const options: EmitOptions = {
|
|
190
|
+
outputDir: '',
|
|
191
|
+
operationRegistry,
|
|
192
|
+
codecTypeImports,
|
|
193
|
+
operationTypeImports,
|
|
194
|
+
extensionIds,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const result1 = await emit(ir, options, mockSqlHook);
|
|
198
|
+
const result2 = await emit(ir, options, mockSqlHook);
|
|
199
|
+
|
|
200
|
+
expect(result1.coreHash).toBe(result2.coreHash);
|
|
201
|
+
expect(result1.contractDts).toBe(result2.contractDts);
|
|
202
|
+
expect(result1.contractJson).toBe(result2.contractJson);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('round-trip: IR → JSON → parse JSON → compare', async () => {
|
|
206
|
+
const ir = createContractIR({
|
|
207
|
+
models: {
|
|
208
|
+
User: {
|
|
209
|
+
storage: { table: 'user' },
|
|
210
|
+
fields: {
|
|
211
|
+
id: { column: 'id' },
|
|
212
|
+
email: { column: 'email' },
|
|
213
|
+
},
|
|
214
|
+
relations: {},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
storage: {
|
|
218
|
+
tables: {
|
|
219
|
+
user: {
|
|
220
|
+
columns: {
|
|
221
|
+
id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
|
|
222
|
+
email: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
|
|
223
|
+
},
|
|
224
|
+
primaryKey: { columns: ['id'] },
|
|
225
|
+
uniques: [],
|
|
226
|
+
indexes: [],
|
|
227
|
+
foreignKeys: [],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
extensionPacks: {
|
|
232
|
+
postgres: {
|
|
233
|
+
version: '0.0.1',
|
|
234
|
+
},
|
|
235
|
+
pg: {},
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Create minimal test data (emitter tests don't load packs)
|
|
240
|
+
const operationRegistry = createOperationRegistry();
|
|
241
|
+
const codecTypeImports: TypesImportSpec[] = [];
|
|
242
|
+
const operationTypeImports: TypesImportSpec[] = [];
|
|
243
|
+
const extensionIds = ['postgres', 'pg'];
|
|
244
|
+
const options: EmitOptions = {
|
|
245
|
+
outputDir: '',
|
|
246
|
+
operationRegistry,
|
|
247
|
+
codecTypeImports,
|
|
248
|
+
operationTypeImports,
|
|
249
|
+
extensionIds,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const result1 = await emit(ir, options, mockSqlHook);
|
|
253
|
+
const contractJson1 = JSON.parse(result1.contractJson) as Record<string, unknown>;
|
|
254
|
+
|
|
255
|
+
const ir2 = createContractIR({
|
|
256
|
+
schemaVersion: contractJson1['schemaVersion'] as string,
|
|
257
|
+
targetFamily: contractJson1['targetFamily'] as string,
|
|
258
|
+
target: contractJson1['target'] as string,
|
|
259
|
+
models: contractJson1['models'] as Record<string, unknown>,
|
|
260
|
+
relations: (contractJson1['relations'] as Record<string, unknown>) || {},
|
|
261
|
+
storage: contractJson1['storage'] as Record<string, unknown>,
|
|
262
|
+
extensionPacks: contractJson1['extensionPacks'] as Record<string, unknown>,
|
|
263
|
+
capabilities:
|
|
264
|
+
(contractJson1['capabilities'] as Record<string, Record<string, boolean>>) || {},
|
|
265
|
+
meta: (contractJson1['meta'] as Record<string, unknown>) || {},
|
|
266
|
+
sources: (contractJson1['sources'] as Record<string, unknown>) || {},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const result2 = await emit(ir2, options, mockSqlHook);
|
|
270
|
+
|
|
271
|
+
expect(result1.contractJson).toBe(result2.contractJson);
|
|
272
|
+
expect(result1.coreHash).toBe(result2.coreHash);
|
|
273
|
+
});
|
|
274
|
+
});
|