@prisma-next/emitter 0.3.0-pr.99.5 → 0.3.0

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.
Files changed (47) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +45 -35
  3. package/dist/domain-type-generation.d.mts +38 -0
  4. package/dist/domain-type-generation.d.mts.map +1 -0
  5. package/dist/domain-type-generation.mjs +255 -0
  6. package/dist/domain-type-generation.mjs.map +1 -0
  7. package/dist/exports/index.d.mts +39 -0
  8. package/dist/exports/index.d.mts.map +1 -0
  9. package/dist/exports/index.mjs +106 -0
  10. package/dist/exports/index.mjs.map +1 -0
  11. package/dist/test/utils.d.mts +21 -0
  12. package/dist/test/utils.d.mts.map +1 -0
  13. package/dist/test/utils.mjs +18 -0
  14. package/dist/test/utils.mjs.map +1 -0
  15. package/dist/type-expression-safety-7_1tfJXA.mjs +8 -0
  16. package/dist/type-expression-safety-7_1tfJXA.mjs.map +1 -0
  17. package/dist/type-expression-safety.d.mts +5 -0
  18. package/dist/type-expression-safety.d.mts.map +1 -0
  19. package/dist/type-expression-safety.mjs +3 -0
  20. package/package.json +27 -11
  21. package/src/domain-type-generation.ts +429 -0
  22. package/src/emit-types.ts +23 -0
  23. package/src/emit.ts +68 -0
  24. package/src/exports/index.ts +14 -9
  25. package/src/generate-contract-dts.ts +117 -0
  26. package/src/type-expression-safety.ts +3 -0
  27. package/test/canonicalization.test.ts +196 -19
  28. package/test/domain-type-generation.test.ts +997 -0
  29. package/test/emitter.integration.test.ts +132 -187
  30. package/test/emitter.roundtrip.test.ts +117 -191
  31. package/test/emitter.test.ts +123 -494
  32. package/test/hashing.test.ts +9 -34
  33. package/test/mock-spi.ts +18 -0
  34. package/test/type-expression-safety.test.ts +34 -0
  35. package/test/utils.ts +30 -165
  36. package/dist/exports/index.js +0 -6
  37. package/dist/exports/index.js.map +0 -1
  38. package/dist/src/exports/index.d.ts +0 -4
  39. package/dist/src/exports/index.d.ts.map +0 -1
  40. package/dist/src/target-family.d.ts +0 -2
  41. package/dist/src/target-family.d.ts.map +0 -1
  42. package/dist/test/utils.d.ts +0 -14
  43. package/dist/test/utils.d.ts.map +0 -1
  44. package/dist/test/utils.js +0 -78
  45. package/dist/test/utils.js.map +0 -1
  46. package/src/target-family.ts +0 -7
  47. package/test/factories.test.ts +0 -274
@@ -1,12 +1,12 @@
1
- import { canonicalizeContract } from '@prisma-next/core-control-plane/emission';
1
+ import { canonicalizeContract } from '@prisma-next/contract/hashing';
2
2
  import { describe, expect, it } from 'vitest';
3
- import { createContractIR } from './utils';
3
+ import { createTestContract } from './utils';
4
4
 
5
5
  describe('canonicalization', () => {
6
6
  it('orders top-level sections correctly', () => {
7
- const ir = createContractIR({
7
+ const ir = createTestContract({
8
8
  capabilities: { postgres: { jsonAgg: true } },
9
- meta: { source: 'test' },
9
+ meta: { emitterVersion: 'test' },
10
10
  });
11
11
 
12
12
  const result = canonicalizeContract(ir);
@@ -29,8 +29,8 @@ describe('canonicalization', () => {
29
29
  expect(capabilitiesIndex).toBeLessThan(metaIndex);
30
30
  });
31
31
 
32
- it('omits nullable false from columns', () => {
33
- const ir = createContractIR({
32
+ it('preserves nullable false on columns', () => {
33
+ const ir = createTestContract({
34
34
  storage: {
35
35
  tables: {
36
36
  user: {
@@ -51,12 +51,76 @@ describe('canonicalization', () => {
51
51
  const columns = user['columns'] as Record<string, unknown>;
52
52
  const id = columns['id'] as Record<string, unknown>;
53
53
  const email = columns['email'] as Record<string, unknown>;
54
- expect(id['nullable']).toBeUndefined();
54
+ expect(id['nullable']).toBe(false);
55
55
  expect(email['nullable']).toBe(true);
56
56
  });
57
57
 
58
+ it('preserves nullable:false for columns with defaults', () => {
59
+ const ir = createTestContract({
60
+ storage: {
61
+ tables: {
62
+ user: {
63
+ columns: {
64
+ created_at: {
65
+ codecId: 'pg/timestamptz@1',
66
+ nativeType: 'timestamptz',
67
+ nullable: false,
68
+ default: { kind: 'function', expression: 'now()' },
69
+ },
70
+ updated_at: {
71
+ codecId: 'pg/timestamptz@1',
72
+ nativeType: 'timestamptz',
73
+ nullable: true,
74
+ },
75
+ },
76
+ },
77
+ },
78
+ },
79
+ });
80
+
81
+ const result = canonicalizeContract(ir);
82
+ const parsed = JSON.parse(result) as Record<string, unknown>;
83
+ const storage = parsed['storage'] as Record<string, unknown>;
84
+ const tables = storage['tables'] as Record<string, unknown>;
85
+ const user = tables['user'] as Record<string, unknown>;
86
+ const columns = user['columns'] as Record<string, unknown>;
87
+ const createdAt = columns['created_at'] as Record<string, unknown>;
88
+ const updatedAt = columns['updated_at'] as Record<string, unknown>;
89
+ expect(createdAt['nullable']).toBe(false);
90
+ expect(updatedAt['nullable']).toBe(true);
91
+ });
92
+
93
+ it('preserves nullable:true for columns with defaults', () => {
94
+ const ir = createTestContract({
95
+ storage: {
96
+ tables: {
97
+ user: {
98
+ columns: {
99
+ bio: {
100
+ codecId: 'pg/text@1',
101
+ nativeType: 'text',
102
+ nullable: true,
103
+ default: { kind: 'literal', value: '' },
104
+ },
105
+ },
106
+ },
107
+ },
108
+ },
109
+ });
110
+
111
+ const result = canonicalizeContract(ir);
112
+ const parsed = JSON.parse(result) as Record<string, unknown>;
113
+ const storage = parsed['storage'] as Record<string, unknown>;
114
+ const tables = storage['tables'] as Record<string, unknown>;
115
+ const user = tables['user'] as Record<string, unknown>;
116
+ const columns = user['columns'] as Record<string, unknown>;
117
+ const bio = columns['bio'] as Record<string, unknown>;
118
+ expect(bio['nullable']).toBe(true);
119
+ expect(bio['default']).toEqual({ kind: 'literal', value: '' });
120
+ });
121
+
58
122
  it('omits empty arrays and objects except required ones', () => {
59
- const ir = createContractIR();
123
+ const ir = createTestContract();
60
124
 
61
125
  const result = canonicalizeContract(ir);
62
126
  const parsed = JSON.parse(result);
@@ -66,19 +130,17 @@ describe('canonicalization', () => {
66
130
  tables: expect.anything(),
67
131
  },
68
132
  });
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
133
+ // Required top-level fields (capabilities, extensionPacks, meta) are preserved even when empty.
71
134
  expect(parsed).toMatchObject({
72
135
  capabilities: expect.anything(),
73
136
  extensionPacks: expect.anything(),
74
137
  meta: expect.anything(),
75
- relations: expect.anything(),
76
- sources: expect.anything(),
77
138
  });
139
+ expect(parsed).not.toHaveProperty('relations');
78
140
  });
79
141
 
80
142
  it('preserves semantic array order for column lists', () => {
81
- const ir = createContractIR({
143
+ const ir = createTestContract({
82
144
  storage: {
83
145
  tables: {
84
146
  user: {
@@ -96,7 +158,7 @@ describe('canonicalization', () => {
96
158
 
97
159
  const result1 = canonicalizeContract(ir);
98
160
 
99
- const ir2 = createContractIR({
161
+ const ir2 = createTestContract({
100
162
  storage: {
101
163
  tables: {
102
164
  user: {
@@ -117,8 +179,8 @@ describe('canonicalization', () => {
117
179
  expect(result1).not.toBe(result2);
118
180
  });
119
181
 
120
- it('sorts non-semantic arrays by canonical name', () => {
121
- const ir = createContractIR({
182
+ it('sorts indexes by canonical name', () => {
183
+ const ir = createTestContract({
122
184
  storage: {
123
185
  tables: {
124
186
  user: {
@@ -144,8 +206,62 @@ describe('canonicalization', () => {
144
206
  expect(indexNames).toEqual(['user_email_idx', 'user_name_idx']);
145
207
  });
146
208
 
209
+ it('sorts uniques by canonical name', () => {
210
+ const ir = createTestContract({
211
+ storage: {
212
+ tables: {
213
+ user: {
214
+ columns: {
215
+ id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
216
+ email: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
217
+ username: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
218
+ },
219
+ uniques: [
220
+ { columns: ['username'], name: 'user_username_key' },
221
+ { columns: ['email'], name: 'user_email_key' },
222
+ ],
223
+ },
224
+ },
225
+ },
226
+ });
227
+
228
+ const result = canonicalizeContract(ir);
229
+ const parsed = JSON.parse(result) as Record<string, unknown>;
230
+ const storage = parsed['storage'] as Record<string, unknown>;
231
+ const tables = storage['tables'] as Record<string, unknown>;
232
+ const user = tables['user'] as Record<string, unknown>;
233
+ const uniques = user['uniques'] as Array<{ name: string }>;
234
+ const uniqueNames = uniques.map((u) => u.name);
235
+ expect(uniqueNames).toEqual(['user_email_key', 'user_username_key']);
236
+ });
237
+
238
+ it('preserves column order in composite unique constraints', () => {
239
+ const ir = createTestContract({
240
+ storage: {
241
+ tables: {
242
+ user: {
243
+ columns: {
244
+ id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
245
+ first_name: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
246
+ last_name: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
247
+ },
248
+ uniques: [{ columns: ['last_name', 'first_name'], name: 'user_name_key' }],
249
+ },
250
+ },
251
+ },
252
+ });
253
+
254
+ const result = canonicalizeContract(ir);
255
+ const parsed = JSON.parse(result) as Record<string, unknown>;
256
+ const storage = parsed['storage'] as Record<string, unknown>;
257
+ const tables = storage['tables'] as Record<string, unknown>;
258
+ const user = tables['user'] as Record<string, unknown>;
259
+ const uniques = user['uniques'] as Array<{ columns: string[] }>;
260
+ expect(uniques[0]!.columns).toEqual(['last_name', 'first_name']);
261
+ });
262
+
147
263
  it('sorts nested object keys lexicographically', () => {
148
- const ir = createContractIR({
264
+ const ir = createTestContract({
149
265
  storage: {
150
266
  tables: {
151
267
  user: {
@@ -169,8 +285,69 @@ describe('canonicalization', () => {
169
285
  expect(columnKeys).toEqual(['a_field', 'm_field', 'z_field']);
170
286
  });
171
287
 
288
+ describe('Mongo storage.collections preservation', () => {
289
+ it('preserves empty storage.collections container', () => {
290
+ const ir = createTestContract({
291
+ targetFamily: 'mongo',
292
+ target: 'mongo',
293
+ storage: { collections: {} },
294
+ });
295
+
296
+ const result = canonicalizeContract(ir);
297
+ const parsed = JSON.parse(result) as Record<string, unknown>;
298
+ const storage = parsed['storage'] as Record<string, unknown>;
299
+ expect(storage['collections']).toEqual({});
300
+ });
301
+
302
+ it('preserves collection entries with empty payloads', () => {
303
+ const ir = createTestContract({
304
+ targetFamily: 'mongo',
305
+ target: 'mongo',
306
+ storage: { collections: { users: {}, posts: {} } },
307
+ });
308
+
309
+ const result = canonicalizeContract(ir);
310
+ const parsed = JSON.parse(result) as Record<string, unknown>;
311
+ const storage = parsed['storage'] as Record<string, unknown>;
312
+ const collections = storage['collections'] as Record<string, unknown>;
313
+ expect(collections['users']).toEqual({});
314
+ expect(collections['posts']).toEqual({});
315
+ });
316
+
317
+ it('sorts collection names lexicographically', () => {
318
+ const ir = createTestContract({
319
+ targetFamily: 'mongo',
320
+ target: 'mongo',
321
+ storage: { collections: { zebras: {}, apples: {}, mangoes: {} } },
322
+ });
323
+
324
+ const result = canonicalizeContract(ir);
325
+ const parsed = JSON.parse(result) as Record<string, unknown>;
326
+ const storage = parsed['storage'] as Record<string, unknown>;
327
+ const collections = storage['collections'] as Record<string, unknown>;
328
+ expect(Object.keys(collections)).toEqual(['apples', 'mangoes', 'zebras']);
329
+ });
330
+
331
+ it('produces different hashes when collections differ', () => {
332
+ const ir1 = createTestContract({
333
+ targetFamily: 'mongo',
334
+ target: 'mongo',
335
+ storage: { collections: { users: {} } },
336
+ });
337
+ const ir2 = createTestContract({
338
+ targetFamily: 'mongo',
339
+ target: 'mongo',
340
+ storage: { collections: { users: {}, posts: {} } },
341
+ });
342
+
343
+ const result1 = canonicalizeContract(ir1);
344
+ const result2 = canonicalizeContract(ir2);
345
+ expect(result1).not.toBe(result2);
346
+ });
347
+ });
348
+
172
349
  it('sorts extension namespaces lexicographically', () => {
173
- const ir = createContractIR({
350
+ const ir = createTestContract({
174
351
  extensionPacks: {
175
352
  pgvector: { version: '0.0.1' },
176
353
  postgres: { version: '0.0.1' },
@@ -186,7 +363,7 @@ describe('canonicalization', () => {
186
363
  });
187
364
 
188
365
  it('omits generated false', () => {
189
- const ir = createContractIR({
366
+ const ir = createTestContract({
190
367
  storage: {
191
368
  tables: {
192
369
  user: {