@prisma-next/emitter 0.3.0-dev.135 → 0.3.0-dev.146

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 (36) hide show
  1. package/README.md +33 -30
  2. package/dist/domain-type-generation.d.mts +14 -2
  3. package/dist/domain-type-generation.d.mts.map +1 -1
  4. package/dist/domain-type-generation.mjs +139 -1
  5. package/dist/domain-type-generation.mjs.map +1 -1
  6. package/dist/exports/index.d.mts +39 -4
  7. package/dist/exports/index.d.mts.map +1 -0
  8. package/dist/exports/index.mjs +104 -3
  9. package/dist/exports/index.mjs.map +1 -0
  10. package/dist/test/utils.d.mts +16 -14
  11. package/dist/test/utils.d.mts.map +1 -1
  12. package/dist/test/utils.mjs +12 -51
  13. package/dist/test/utils.mjs.map +1 -1
  14. package/dist/type-expression-safety-7_1tfJXA.mjs +8 -0
  15. package/dist/type-expression-safety-7_1tfJXA.mjs.map +1 -0
  16. package/dist/type-expression-safety.d.mts +5 -0
  17. package/dist/type-expression-safety.d.mts.map +1 -0
  18. package/dist/type-expression-safety.mjs +3 -0
  19. package/package.json +12 -6
  20. package/src/domain-type-generation.ts +227 -1
  21. package/src/emit-types.ts +23 -0
  22. package/src/emit.ts +68 -0
  23. package/src/exports/index.ts +4 -9
  24. package/src/generate-contract-dts.ts +116 -0
  25. package/src/type-expression-safety.ts +3 -0
  26. package/test/canonicalization.test.ts +25 -28
  27. package/test/domain-type-generation.test.ts +509 -2
  28. package/test/emitter.integration.test.ts +81 -139
  29. package/test/emitter.roundtrip.test.ts +114 -184
  30. package/test/emitter.test.ts +82 -467
  31. package/test/hashing.test.ts +8 -30
  32. package/test/mock-spi.ts +18 -0
  33. package/test/type-expression-safety.test.ts +34 -0
  34. package/test/utils.ts +30 -156
  35. package/src/target-family.ts +0 -7
  36. package/test/factories.test.ts +0 -274
@@ -1,19 +1,21 @@
1
- import { ContractIR } from "@prisma-next/contract/ir";
1
+ import { Contract } from "@prisma-next/contract/types";
2
2
 
3
3
  //#region test/utils.d.ts
4
-
5
- /**
6
- * Factory function for creating ContractIR objects in tests.
7
- * Provides sensible defaults and allows overriding specific fields.
8
- * Uses the emitter factories internally for consistency.
9
- *
10
- * If a field is explicitly set to `undefined` in overrides, it will be omitted
11
- * from the result (useful for testing validation of missing fields).
12
- */
13
- declare function createContractIR(overrides?: Partial<ContractIR> & {
4
+ type TestContractOverrides = {
5
+ target?: string;
6
+ targetFamily?: string;
7
+ roots?: Record<string, string>;
8
+ models?: Record<string, unknown>;
9
+ storage?: Record<string, unknown>;
10
+ capabilities?: Record<string, Record<string, boolean>>;
11
+ extensionPacks?: Record<string, unknown>;
12
+ execution?: Record<string, unknown>;
13
+ meta?: Record<string, unknown>;
14
14
  storageHash?: string;
15
- profileHash?: string;
16
- }): ContractIR;
15
+ schemaVersion?: string;
16
+ sources?: Record<string, unknown>;
17
+ };
18
+ declare function createTestContract(overrides?: TestContractOverrides): Contract;
17
19
  //#endregion
18
- export { createContractIR };
20
+ export { createTestContract };
19
21
  //# sourceMappingURL=utils.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.mts","names":[],"sources":["../../test/utils.ts"],"sourcesContent":[],"mappings":";;;;;;AAUA;;;;;;iBAAgB,gBAAA,aACH,QAAQ;;;IAClB"}
1
+ {"version":3,"file":"utils.d.mts","names":[],"sources":["../../test/utils.ts"],"sourcesContent":[],"mappings":";;;KAGK,qBAAA;;EAAA,YAAA,CAAA,EAAA,MAAA;EAGK,KAAA,CAAA,EAAA,MAAA,CAAA,MAAA,EAAA,MAAA,CAAA;EACC,MAAA,CAAA,EAAA,MAAA,CAAA,MAAA,EAAA,OAAA,CAAA;EACC,OAAA,CAAA,EAAA,MAAA,CAAA,MAAA,EAAA,OAAA,CAAA;EACoB,YAAA,CAAA,EAAf,MAAe,CAAA,MAAA,EAAA,MAAA,CAAA,MAAA,EAAA,OAAA,CAAA,CAAA;EAAf,cAAA,CAAA,EACE,MADF,CAAA,MAAA,EAAA,OAAA,CAAA;EACE,SAAA,CAAA,EACL,MADK,CAAA,MAAA,EAAA,OAAA,CAAA;EACL,IAAA,CAAA,EACL,MADK,CAAA,MAAA,EAAA,OAAA,CAAA;EACL,WAAA,CAAA,EAAA,MAAA;EAGG,aAAA,CAAA,EAAA,MAAA;EAAM,OAAA,CAAA,EAAN,MAAM,CAAA,MAAA,EAAA,OAAA,CAAA;AAGlB,CAAA;iBAAgB,kBAAA,aAA8B,wBAA6B"}
@@ -1,57 +1,18 @@
1
- import { irHeader, irMeta } from "@prisma-next/contract/ir";
1
+ import { createContract } from "@prisma-next/contract/testing";
2
2
 
3
3
  //#region test/utils.ts
4
- /**
5
- * Factory function for creating ContractIR objects in tests.
6
- * Provides sensible defaults and allows overriding specific fields.
7
- * Uses the emitter factories internally for consistency.
8
- *
9
- * If a field is explicitly set to `undefined` in overrides, it will be omitted
10
- * from the result (useful for testing validation of missing fields).
11
- */
12
- function createContractIR(overrides = {}) {
13
- const hasTarget = "target" in overrides;
14
- const hasTargetFamily = "targetFamily" in overrides;
15
- const hasStorageHash = "storageHash" in overrides;
16
- const hasSchemaVersion = "schemaVersion" in overrides;
17
- const hasModels = "models" in overrides;
18
- const hasStorage = "storage" in overrides;
19
- const hasCapabilities = "capabilities" in overrides;
20
- const hasExtensionPacks = "extensionPacks" in overrides;
21
- const hasMeta = "meta" in overrides;
22
- const hasSources = "sources" in overrides;
23
- const headerOpts = {};
24
- if (hasTarget && overrides.target !== void 0) headerOpts.target = overrides.target;
25
- else if (!hasTarget) headerOpts.target = "postgres";
26
- if (hasTargetFamily && overrides.targetFamily !== void 0) headerOpts.targetFamily = overrides.targetFamily;
27
- else if (!hasTargetFamily) headerOpts.targetFamily = "sql";
28
- if (hasStorageHash && overrides.storageHash !== void 0) headerOpts.storageHash = overrides.storageHash;
29
- else if (!hasStorageHash) headerOpts.storageHash = "sha256:test";
30
- if (overrides.profileHash !== void 0) headerOpts.profileHash = overrides.profileHash;
31
- const header = irHeader(headerOpts);
32
- const metaOpts = {};
33
- if (hasCapabilities && overrides.capabilities !== void 0) metaOpts.capabilities = overrides.capabilities;
34
- else if (!hasCapabilities) metaOpts.capabilities = {};
35
- if (hasExtensionPacks && overrides.extensionPacks !== void 0) metaOpts.extensionPacks = overrides.extensionPacks;
36
- else if (!hasExtensionPacks) metaOpts.extensionPacks = {};
37
- if (hasMeta && overrides.meta !== void 0) metaOpts.meta = overrides.meta;
38
- else if (!hasMeta) metaOpts.meta = {};
39
- if (hasSources && overrides.sources !== void 0) metaOpts.sources = overrides.sources;
40
- else if (!hasSources) metaOpts.sources = {};
41
- const meta = irMeta(Object.keys(metaOpts).length > 0 ? metaOpts : void 0);
42
- return {
43
- schemaVersion: hasSchemaVersion && overrides.schemaVersion !== void 0 ? overrides.schemaVersion : hasSchemaVersion && overrides.schemaVersion === void 0 ? void 0 : header.schemaVersion,
44
- target: header.target,
45
- targetFamily: header.targetFamily,
46
- capabilities: hasCapabilities && overrides.capabilities === void 0 ? void 0 : !hasCapabilities || overrides.capabilities !== void 0 ? meta.capabilities : {},
47
- extensionPacks: hasExtensionPacks && overrides.extensionPacks === void 0 ? void 0 : !hasExtensionPacks || overrides.extensionPacks !== void 0 ? meta.extensionPacks : {},
48
- meta: hasMeta && overrides.meta === void 0 ? void 0 : !hasMeta || overrides.meta !== void 0 ? meta.meta : {},
49
- sources: hasSources && overrides.sources === void 0 ? void 0 : !hasSources || overrides.sources !== void 0 ? meta.sources : {},
50
- storage: hasStorage && overrides.storage === void 0 ? void 0 : hasStorage && overrides.storage !== void 0 ? overrides.storage : !hasStorage ? { tables: {} } : {},
51
- models: hasModels && overrides.models === void 0 ? void 0 : hasModels && overrides.models !== void 0 ? overrides.models : !hasModels ? {} : {}
52
- };
4
+ function createTestContract(overrides = {}) {
5
+ const { storageHash: _sh, schemaVersion: _sv, sources: _src, storage, ...rest } = overrides;
6
+ const cleanStorage = storage ? (() => {
7
+ const { storageHash: _innerSh, ...storageRest } = storage;
8
+ return storageRest;
9
+ })() : void 0;
10
+ return createContract({
11
+ ...rest,
12
+ ...cleanStorage ? { storage: cleanStorage } : {}
13
+ });
53
14
  }
54
15
 
55
16
  //#endregion
56
- export { createContractIR };
17
+ export { createTestContract };
57
18
  //# sourceMappingURL=utils.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.mjs","names":["headerOpts: {\n target?: string;\n targetFamily?: string;\n storageHash?: string;\n profileHash?: string;\n }","metaOpts: {\n capabilities?: Record<string, Record<string, boolean>>;\n extensionPacks?: Record<string, unknown>;\n meta?: Record<string, unknown>;\n sources?: Record<string, unknown>;\n }"],"sources":["../../test/utils.ts"],"sourcesContent":["import { type ContractIR, irHeader, irMeta } from '@prisma-next/contract/ir';\n\n/**\n * Factory function for creating ContractIR objects in tests.\n * Provides sensible defaults and allows overriding specific fields.\n * Uses the emitter factories internally for consistency.\n *\n * If a field is explicitly set to `undefined` in overrides, it will be omitted\n * from the result (useful for testing validation of missing fields).\n */\nexport function createContractIR(\n overrides: Partial<ContractIR> & { storageHash?: string; profileHash?: string } = {},\n): ContractIR {\n // Check if fields are explicitly undefined (not just missing)\n const hasTarget = 'target' in overrides;\n const hasTargetFamily = 'targetFamily' in overrides;\n const hasStorageHash = 'storageHash' in overrides;\n const hasSchemaVersion = 'schemaVersion' in overrides;\n const hasModels = 'models' in overrides;\n const hasStorage = 'storage' in overrides;\n const hasCapabilities = 'capabilities' in overrides;\n const hasExtensionPacks = 'extensionPacks' in overrides;\n const hasMeta = 'meta' in overrides;\n const hasSources = 'sources' in overrides;\n\n // Build header, omitting fields that are explicitly undefined\n const headerOpts: {\n target?: string;\n targetFamily?: string;\n storageHash?: string;\n profileHash?: string;\n } = {};\n\n if (hasTarget && overrides.target !== undefined) {\n headerOpts.target = overrides.target;\n } else if (!hasTarget) {\n headerOpts.target = 'postgres';\n }\n\n if (hasTargetFamily && overrides.targetFamily !== undefined) {\n headerOpts.targetFamily = overrides.targetFamily;\n } else if (!hasTargetFamily) {\n headerOpts.targetFamily = 'sql';\n }\n\n if (hasStorageHash && overrides.storageHash !== undefined) {\n headerOpts.storageHash = overrides.storageHash;\n } else if (!hasStorageHash) {\n headerOpts.storageHash = 'sha256:test';\n }\n\n // profileHash is not part of ContractIR, but we can accept it for header creation\n if (overrides.profileHash !== undefined) {\n headerOpts.profileHash = overrides.profileHash;\n }\n\n const header = irHeader(\n headerOpts as {\n target: string;\n targetFamily: string;\n storageHash: string;\n profileHash?: string;\n },\n );\n\n // Build meta, handling explicitly undefined fields\n // If a field is explicitly undefined, we'll omit it from the result later\n const metaOpts: {\n capabilities?: Record<string, Record<string, boolean>>;\n extensionPacks?: Record<string, unknown>;\n meta?: Record<string, unknown>;\n sources?: Record<string, unknown>;\n } = {};\n\n if (hasCapabilities && overrides.capabilities !== undefined) {\n metaOpts.capabilities = overrides.capabilities;\n } else if (!hasCapabilities) {\n metaOpts.capabilities = {};\n }\n\n if (hasExtensionPacks && overrides.extensionPacks !== undefined) {\n metaOpts.extensionPacks = overrides.extensionPacks;\n } else if (!hasExtensionPacks) {\n metaOpts.extensionPacks = {};\n }\n\n if (hasMeta && overrides.meta !== undefined) {\n metaOpts.meta = overrides.meta;\n } else if (!hasMeta) {\n metaOpts.meta = {};\n }\n\n if (hasSources && overrides.sources !== undefined) {\n metaOpts.sources = overrides.sources;\n } else if (!hasSources) {\n metaOpts.sources = {};\n }\n\n const meta = irMeta(Object.keys(metaOpts).length > 0 ? metaOpts : undefined);\n\n // Build result by constructing the object directly (ContractIR doesn't include storageHash/profileHash)\n // When fields are explicitly undefined, include them as undefined (tests use type assertions to bypass TS)\n const result = {\n schemaVersion:\n hasSchemaVersion && overrides.schemaVersion !== undefined\n ? overrides.schemaVersion\n : hasSchemaVersion && overrides.schemaVersion === undefined\n ? (undefined as unknown as string)\n : header.schemaVersion,\n target: header.target,\n targetFamily: header.targetFamily,\n // Only include meta fields if they're not explicitly undefined\n capabilities:\n hasCapabilities && overrides.capabilities === undefined\n ? (undefined as unknown as Record<string, Record<string, boolean>>)\n : !hasCapabilities || overrides.capabilities !== undefined\n ? meta.capabilities\n : ({} as Record<string, Record<string, boolean>>),\n extensionPacks:\n hasExtensionPacks && overrides.extensionPacks === undefined\n ? (undefined as unknown as Record<string, unknown>)\n : !hasExtensionPacks || overrides.extensionPacks !== undefined\n ? meta.extensionPacks\n : ({} as Record<string, unknown>),\n meta:\n hasMeta && overrides.meta === undefined\n ? (undefined as unknown as Record<string, unknown>)\n : !hasMeta || overrides.meta !== undefined\n ? meta.meta\n : ({} as Record<string, unknown>),\n sources:\n hasSources && overrides.sources === undefined\n ? (undefined as unknown as Record<string, unknown>)\n : !hasSources || overrides.sources !== undefined\n ? meta.sources\n : ({} as Record<string, unknown>),\n // Only include family sections if they're not explicitly undefined\n storage:\n hasStorage && overrides.storage === undefined\n ? (undefined as unknown as Record<string, unknown>)\n : hasStorage && overrides.storage !== undefined\n ? (overrides.storage as Record<string, unknown>)\n : !hasStorage\n ? ({ tables: {} } as Record<string, unknown>)\n : ({} as Record<string, unknown>),\n models:\n hasModels && overrides.models === undefined\n ? (undefined as unknown as Record<string, unknown>)\n : hasModels && overrides.models !== undefined\n ? (overrides.models as Record<string, unknown>)\n : !hasModels\n ? {}\n : ({} as Record<string, unknown>),\n } as ContractIR;\n\n return result;\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAgB,iBACd,YAAkF,EAAE,EACxE;CAEZ,MAAM,YAAY,YAAY;CAC9B,MAAM,kBAAkB,kBAAkB;CAC1C,MAAM,iBAAiB,iBAAiB;CACxC,MAAM,mBAAmB,mBAAmB;CAC5C,MAAM,YAAY,YAAY;CAC9B,MAAM,aAAa,aAAa;CAChC,MAAM,kBAAkB,kBAAkB;CAC1C,MAAM,oBAAoB,oBAAoB;CAC9C,MAAM,UAAU,UAAU;CAC1B,MAAM,aAAa,aAAa;CAGhC,MAAMA,aAKF,EAAE;AAEN,KAAI,aAAa,UAAU,WAAW,OACpC,YAAW,SAAS,UAAU;UACrB,CAAC,UACV,YAAW,SAAS;AAGtB,KAAI,mBAAmB,UAAU,iBAAiB,OAChD,YAAW,eAAe,UAAU;UAC3B,CAAC,gBACV,YAAW,eAAe;AAG5B,KAAI,kBAAkB,UAAU,gBAAgB,OAC9C,YAAW,cAAc,UAAU;UAC1B,CAAC,eACV,YAAW,cAAc;AAI3B,KAAI,UAAU,gBAAgB,OAC5B,YAAW,cAAc,UAAU;CAGrC,MAAM,SAAS,SACb,WAMD;CAID,MAAMC,WAKF,EAAE;AAEN,KAAI,mBAAmB,UAAU,iBAAiB,OAChD,UAAS,eAAe,UAAU;UACzB,CAAC,gBACV,UAAS,eAAe,EAAE;AAG5B,KAAI,qBAAqB,UAAU,mBAAmB,OACpD,UAAS,iBAAiB,UAAU;UAC3B,CAAC,kBACV,UAAS,iBAAiB,EAAE;AAG9B,KAAI,WAAW,UAAU,SAAS,OAChC,UAAS,OAAO,UAAU;UACjB,CAAC,QACV,UAAS,OAAO,EAAE;AAGpB,KAAI,cAAc,UAAU,YAAY,OACtC,UAAS,UAAU,UAAU;UACpB,CAAC,WACV,UAAS,UAAU,EAAE;CAGvB,MAAM,OAAO,OAAO,OAAO,KAAK,SAAS,CAAC,SAAS,IAAI,WAAW,OAAU;AAyD5E,QArDe;EACb,eACE,oBAAoB,UAAU,kBAAkB,SAC5C,UAAU,gBACV,oBAAoB,UAAU,kBAAkB,SAC7C,SACD,OAAO;EACf,QAAQ,OAAO;EACf,cAAc,OAAO;EAErB,cACE,mBAAmB,UAAU,iBAAiB,SACzC,SACD,CAAC,mBAAmB,UAAU,iBAAiB,SAC7C,KAAK,eACJ,EAAE;EACX,gBACE,qBAAqB,UAAU,mBAAmB,SAC7C,SACD,CAAC,qBAAqB,UAAU,mBAAmB,SACjD,KAAK,iBACJ,EAAE;EACX,MACE,WAAW,UAAU,SAAS,SACzB,SACD,CAAC,WAAW,UAAU,SAAS,SAC7B,KAAK,OACJ,EAAE;EACX,SACE,cAAc,UAAU,YAAY,SAC/B,SACD,CAAC,cAAc,UAAU,YAAY,SACnC,KAAK,UACJ,EAAE;EAEX,SACE,cAAc,UAAU,YAAY,SAC/B,SACD,cAAc,UAAU,YAAY,SACjC,UAAU,UACX,CAAC,aACE,EAAE,QAAQ,EAAE,EAAE,GACd,EAAE;EACb,QACE,aAAa,UAAU,WAAW,SAC7B,SACD,aAAa,UAAU,WAAW,SAC/B,UAAU,SACX,CAAC,YACC,EAAE,GACD,EAAE;EACd"}
1
+ {"version":3,"file":"utils.mjs","names":[],"sources":["../../test/utils.ts"],"sourcesContent":["import { createContract } from '@prisma-next/contract/testing';\nimport type { Contract } from '@prisma-next/contract/types';\n\ntype TestContractOverrides = {\n target?: string;\n targetFamily?: string;\n roots?: Record<string, string>;\n models?: Record<string, unknown>;\n storage?: Record<string, unknown>;\n capabilities?: Record<string, Record<string, boolean>>;\n extensionPacks?: Record<string, unknown>;\n execution?: Record<string, unknown>;\n meta?: Record<string, unknown>;\n storageHash?: string;\n schemaVersion?: string;\n sources?: Record<string, unknown>;\n};\n\nexport function createTestContract(overrides: TestContractOverrides = {}): Contract {\n const { storageHash: _sh, schemaVersion: _sv, sources: _src, storage, ...rest } = overrides;\n const cleanStorage = storage\n ? (() => {\n const { storageHash: _innerSh, ...storageRest } = storage as Record<string, unknown>;\n return storageRest;\n })()\n : undefined;\n return createContract({\n ...rest,\n ...(cleanStorage ? { storage: cleanStorage } : {}),\n } as Parameters<typeof createContract>[0]);\n}\n"],"mappings":";;;AAkBA,SAAgB,mBAAmB,YAAmC,EAAE,EAAY;CAClF,MAAM,EAAE,aAAa,KAAK,eAAe,KAAK,SAAS,MAAM,SAAS,GAAG,SAAS;CAClF,MAAM,eAAe,iBACV;EACL,MAAM,EAAE,aAAa,UAAU,GAAG,gBAAgB;AAClD,SAAO;KACL,GACJ;AACJ,QAAO,eAAe;EACpB,GAAG;EACH,GAAI,eAAe,EAAE,SAAS,cAAc,GAAG,EAAE;EAClD,CAAyC"}
@@ -0,0 +1,8 @@
1
+ //#region src/type-expression-safety.ts
2
+ function isSafeTypeExpression(expr) {
3
+ return !/import\s*\(|require\s*\(|declare\s|export\s|eval\s*\(/.test(expr);
4
+ }
5
+
6
+ //#endregion
7
+ export { isSafeTypeExpression as t };
8
+ //# sourceMappingURL=type-expression-safety-7_1tfJXA.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"type-expression-safety-7_1tfJXA.mjs","names":[],"sources":["../src/type-expression-safety.ts"],"sourcesContent":["export function isSafeTypeExpression(expr: string): boolean {\n return !/import\\s*\\(|require\\s*\\(|declare\\s|export\\s|eval\\s*\\(/.test(expr);\n}\n"],"mappings":";AAAA,SAAgB,qBAAqB,MAAuB;AAC1D,QAAO,CAAC,wDAAwD,KAAK,KAAK"}
@@ -0,0 +1,5 @@
1
+ //#region src/type-expression-safety.d.ts
2
+ declare function isSafeTypeExpression(expr: string): boolean;
3
+ //#endregion
4
+ export { isSafeTypeExpression };
5
+ //# sourceMappingURL=type-expression-safety.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"type-expression-safety.d.mts","names":[],"sources":["../src/type-expression-safety.ts"],"sourcesContent":[],"mappings":";iBAAgB,oBAAA"}
@@ -0,0 +1,3 @@
1
+ import { t as isSafeTypeExpression } from "./type-expression-safety-7_1tfJXA.mjs";
2
+
3
+ export { isSafeTypeExpression };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma-next/emitter",
3
- "version": "0.3.0-dev.135",
3
+ "version": "0.3.0-dev.146",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "files": [
@@ -10,18 +10,20 @@
10
10
  ],
11
11
  "dependencies": {
12
12
  "arktype": "^2.0.0",
13
- "@prisma-next/contract": "0.3.0-dev.135",
14
- "@prisma-next/core-control-plane": "0.3.0-dev.135"
13
+ "prettier": "^3.3.3",
14
+ "@prisma-next/contract": "0.3.0-dev.146",
15
+ "@prisma-next/operations": "0.3.0-dev.146",
16
+ "@prisma-next/framework-components": "0.3.0-dev.146",
17
+ "@prisma-next/utils": "0.3.0-dev.146"
15
18
  },
16
19
  "devDependencies": {
17
20
  "@types/node": "24.10.4",
18
21
  "tsdown": "0.18.4",
19
22
  "typescript": "5.9.3",
20
23
  "vitest": "4.0.17",
21
- "@prisma-next/operations": "0.3.0-dev.135",
22
- "@prisma-next/test-utils": "0.0.1",
24
+ "@prisma-next/tsconfig": "0.0.0",
23
25
  "@prisma-next/tsdown": "0.0.0",
24
- "@prisma-next/tsconfig": "0.0.0"
26
+ "@prisma-next/test-utils": "0.0.1"
25
27
  },
26
28
  "exports": {
27
29
  ".": {
@@ -32,6 +34,10 @@
32
34
  "types": "./dist/domain-type-generation.d.mts",
33
35
  "import": "./dist/domain-type-generation.mjs"
34
36
  },
37
+ "./type-expression-safety": {
38
+ "types": "./dist/type-expression-safety.d.mts",
39
+ "import": "./dist/type-expression-safety.mjs"
40
+ },
35
41
  "./test/utils": {
36
42
  "types": "./dist/test/utils.d.mts",
37
43
  "import": "./dist/test/utils.mjs"
@@ -1,4 +1,11 @@
1
- import type { TypesImportSpec } from '@prisma-next/contract/types';
1
+ import type {
2
+ ContractField,
3
+ ContractModel,
4
+ ContractValueObject,
5
+ } from '@prisma-next/contract/types';
6
+ import type { CodecLookup } from '@prisma-next/framework-components/codec';
7
+ import type { TypesImportSpec } from '@prisma-next/framework-components/emission';
8
+ import { isSafeTypeExpression } from './type-expression-safety';
2
9
 
3
10
  export function serializeValue(value: unknown): string {
4
11
  if (value === null) {
@@ -48,6 +55,36 @@ export function generateRootsType(roots: Record<string, string> | undefined): st
48
55
  return `{ ${entries} }`;
49
56
  }
50
57
 
58
+ function contractFieldModifierSuffix(field: ContractField): string {
59
+ const many = field.many === true ? '; readonly many: true' : '';
60
+ const dict = field.dict === true ? '; readonly dict: true' : '';
61
+ return many + dict;
62
+ }
63
+
64
+ export function generateModelFieldEntry(fieldName: string, field: ContractField): string {
65
+ const mods = contractFieldModifierSuffix(field);
66
+ const { nullable, type } = field;
67
+ if (type.kind === 'scalar') {
68
+ const typeParamsSpec =
69
+ type.typeParams && Object.keys(type.typeParams).length > 0
70
+ ? `; readonly typeParams: ${serializeValue(type.typeParams)}`
71
+ : '';
72
+ return `readonly ${serializeObjectKey(fieldName)}: { readonly nullable: ${nullable}; readonly type: { readonly kind: 'scalar'; readonly codecId: ${serializeValue(type.codecId)}${typeParamsSpec} }${mods} }`;
73
+ }
74
+ if (type.kind === 'valueObject') {
75
+ return `readonly ${serializeObjectKey(fieldName)}: { readonly nullable: ${nullable}; readonly type: { readonly kind: 'valueObject'; readonly name: ${serializeValue(type.name)} }${mods} }`;
76
+ }
77
+ return `readonly ${serializeObjectKey(fieldName)}: { readonly nullable: ${nullable}; readonly type: ${serializeValue(type)}${mods} }`;
78
+ }
79
+
80
+ export function generateModelFieldsType(fields: Record<string, ContractField>): string {
81
+ const fieldEntries: string[] = [];
82
+ for (const [fieldName, field] of Object.entries(fields)) {
83
+ fieldEntries.push(generateModelFieldEntry(fieldName, field));
84
+ }
85
+ return fieldEntries.length > 0 ? `{ ${fieldEntries.join('; ')} }` : 'Record<string, never>';
86
+ }
87
+
51
88
  export function generateModelRelationsType(relations: Record<string, unknown>): string {
52
89
  const relationEntries: string[] = [];
53
90
 
@@ -86,6 +123,45 @@ export function generateModelRelationsType(relations: Record<string, unknown>):
86
123
  return `{ ${relationEntries.join('; ')} }`;
87
124
  }
88
125
 
126
+ export function generateModelsType(
127
+ models: Record<string, ContractModel>,
128
+ generateModelStorage: (modelName: string, model: ContractModel) => string,
129
+ ): string {
130
+ if (!models || Object.keys(models).length === 0) {
131
+ return 'Record<string, never>';
132
+ }
133
+
134
+ const modelTypes: string[] = [];
135
+ for (const [modelName, model] of Object.entries(models).sort(([a], [b]) => a.localeCompare(b))) {
136
+ const fieldsType = generateModelFieldsType(model.fields);
137
+ const relationsType = generateModelRelationsType(model.relations);
138
+ const storageType = generateModelStorage(modelName, model);
139
+
140
+ const modelParts: string[] = [
141
+ `readonly fields: ${fieldsType}`,
142
+ `readonly relations: ${relationsType}`,
143
+ `readonly storage: ${storageType}`,
144
+ ];
145
+
146
+ if (model.owner) {
147
+ modelParts.push(`readonly owner: ${serializeValue(model.owner)}`);
148
+ }
149
+ if (model.discriminator) {
150
+ modelParts.push(`readonly discriminator: ${serializeValue(model.discriminator)}`);
151
+ }
152
+ if (model.variants) {
153
+ modelParts.push(`readonly variants: ${serializeValue(model.variants)}`);
154
+ }
155
+ if (model.base) {
156
+ modelParts.push(`readonly base: ${serializeValue(model.base)}`);
157
+ }
158
+
159
+ modelTypes.push(`readonly ${modelName}: { ${modelParts.join('; ')} }`);
160
+ }
161
+
162
+ return `{ ${modelTypes.join('; ')} }`;
163
+ }
164
+
89
165
  export function deduplicateImports(imports: TypesImportSpec[]): TypesImportSpec[] {
90
166
  const seenKeys = new Set<string>();
91
167
  const result: TypesImportSpec[] = [];
@@ -114,6 +190,15 @@ export function generateCodecTypeIntersection(
114
190
  return aliases.join(' & ') || 'Record<string, never>';
115
191
  }
116
192
 
193
+ export function serializeExecutionType(execution: Record<string, unknown>): string {
194
+ const parts: string[] = ['readonly executionHash: ExecutionHash'];
195
+ for (const [key, value] of Object.entries(execution)) {
196
+ if (key === 'executionHash') continue;
197
+ parts.push(`readonly ${serializeObjectKey(key)}: ${serializeValue(value)}`);
198
+ }
199
+ return `{ ${parts.join('; ')} }`;
200
+ }
201
+
117
202
  export function generateHashTypeAliases(hashes: {
118
203
  readonly storageHash: string;
119
204
  readonly executionHash?: string;
@@ -129,3 +214,144 @@ export function generateHashTypeAliases(hashes: {
129
214
  `export type ProfileHash = ProfileHashBase<'${hashes.profileHash}'>;`,
130
215
  ].join('\n');
131
216
  }
217
+
218
+ export function generateFieldResolvedType(field: ContractField, codecLookup?: CodecLookup): string {
219
+ let baseType: string;
220
+ const { type } = field;
221
+
222
+ switch (type.kind) {
223
+ case 'scalar': {
224
+ let resolved: string | undefined;
225
+ if (codecLookup && type.typeParams && Object.keys(type.typeParams).length > 0) {
226
+ const codec = codecLookup.get(type.codecId);
227
+ if (codec?.renderOutputType) {
228
+ const rendered = codec.renderOutputType(type.typeParams);
229
+ if (rendered && isSafeTypeExpression(rendered)) {
230
+ resolved = rendered;
231
+ }
232
+ }
233
+ }
234
+ baseType = resolved ?? `CodecTypes[${serializeValue(type.codecId)}]['output']`;
235
+ break;
236
+ }
237
+ case 'valueObject':
238
+ baseType = type.name;
239
+ break;
240
+ case 'union': {
241
+ const memberTypes = type.members.map((m) => {
242
+ if (m.kind === 'scalar') return `CodecTypes[${serializeValue(m.codecId)}]['output']`;
243
+ return m.name;
244
+ });
245
+ baseType = memberTypes.join(' | ');
246
+ break;
247
+ }
248
+ default:
249
+ baseType = 'unknown';
250
+ break;
251
+ }
252
+
253
+ if (field.many === true) {
254
+ baseType = `ReadonlyArray<${baseType}>`;
255
+ }
256
+ if (field.dict === true) {
257
+ baseType = `Readonly<Record<string, ${baseType}>>`;
258
+ }
259
+ if (field.nullable) {
260
+ baseType = `${baseType} | null`;
261
+ }
262
+
263
+ return baseType;
264
+ }
265
+
266
+ export function generateFieldOutputTypesMap(
267
+ models: Record<string, ContractModel> | undefined,
268
+ codecLookup?: CodecLookup,
269
+ ): string {
270
+ if (!models || Object.keys(models).length === 0) {
271
+ return 'Record<string, never>';
272
+ }
273
+
274
+ const modelEntries: string[] = [];
275
+ for (const [modelName, model] of Object.entries(models).sort(([a], [b]) => a.localeCompare(b))) {
276
+ if (!model) continue;
277
+ const fieldEntries: string[] = [];
278
+ for (const [fieldName, field] of Object.entries(model.fields)) {
279
+ const resolvedType = generateFieldResolvedType(field, codecLookup);
280
+ fieldEntries.push(`readonly ${serializeObjectKey(fieldName)}: ${resolvedType}`);
281
+ }
282
+ const fieldsType =
283
+ fieldEntries.length > 0 ? `{ ${fieldEntries.join('; ')} }` : 'Record<string, never>';
284
+ modelEntries.push(`readonly ${serializeObjectKey(modelName)}: ${fieldsType}`);
285
+ }
286
+
287
+ return `{ ${modelEntries.join('; ')} }`;
288
+ }
289
+
290
+ export function generateValueObjectType(
291
+ _voName: string,
292
+ vo: ContractValueObject,
293
+ _valueObjects: Record<string, ContractValueObject>,
294
+ ): string {
295
+ const fieldEntries: string[] = [];
296
+ for (const [fieldName, field] of Object.entries(vo.fields)) {
297
+ const tsType = generateFieldResolvedType(field);
298
+ fieldEntries.push(`readonly ${serializeObjectKey(fieldName)}: ${tsType}`);
299
+ }
300
+ return fieldEntries.length > 0 ? `{ ${fieldEntries.join('; ')} }` : 'Record<string, never>';
301
+ }
302
+
303
+ export function generateContractFieldDescriptor(fieldName: string, field: ContractField): string {
304
+ const mods: string[] = [];
305
+ if (field.many === true) mods.push('; readonly many: true');
306
+ if (field.dict === true) mods.push('; readonly dict: true');
307
+ const modStr = mods.join('');
308
+
309
+ const { type } = field;
310
+ if (type.kind === 'scalar') {
311
+ const typeParamsSpec =
312
+ type.typeParams && Object.keys(type.typeParams).length > 0
313
+ ? `; readonly typeParams: ${serializeValue(type.typeParams)}`
314
+ : '';
315
+ return `readonly ${serializeObjectKey(fieldName)}: { readonly nullable: ${field.nullable}; readonly type: { readonly kind: 'scalar'; readonly codecId: ${serializeValue(type.codecId)}${typeParamsSpec} }${modStr} }`;
316
+ }
317
+ if (type.kind === 'valueObject') {
318
+ return `readonly ${serializeObjectKey(fieldName)}: { readonly nullable: ${field.nullable}; readonly type: { readonly kind: 'valueObject'; readonly name: ${serializeValue(type.name)} }${modStr} }`;
319
+ }
320
+ return `readonly ${serializeObjectKey(fieldName)}: { readonly nullable: ${field.nullable}; readonly type: ${serializeValue(type)}${modStr} }`;
321
+ }
322
+
323
+ export function generateValueObjectsDescriptorType(
324
+ valueObjects: Record<string, ContractValueObject> | undefined,
325
+ ): string {
326
+ if (!valueObjects || Object.keys(valueObjects).length === 0) {
327
+ return 'Record<string, never>';
328
+ }
329
+
330
+ const voEntries: string[] = [];
331
+ for (const [voName, vo] of Object.entries(valueObjects)) {
332
+ const fieldEntries: string[] = [];
333
+ for (const [fieldName, field] of Object.entries(vo.fields)) {
334
+ fieldEntries.push(generateContractFieldDescriptor(fieldName, field));
335
+ }
336
+ const fieldsType =
337
+ fieldEntries.length > 0 ? `{ ${fieldEntries.join('; ')} }` : 'Record<string, never>';
338
+ voEntries.push(`readonly ${serializeObjectKey(voName)}: { readonly fields: ${fieldsType} }`);
339
+ }
340
+
341
+ return `{ ${voEntries.join('; ')} }`;
342
+ }
343
+
344
+ export function generateValueObjectTypeAliases(
345
+ valueObjects: Record<string, ContractValueObject> | undefined,
346
+ ): string {
347
+ if (!valueObjects || Object.keys(valueObjects).length === 0) {
348
+ return '';
349
+ }
350
+
351
+ const aliases: string[] = [];
352
+ for (const [voName, vo] of Object.entries(valueObjects)) {
353
+ const voType = generateValueObjectType(voName, vo, valueObjects);
354
+ aliases.push(`export type ${voName} = ${voType};`);
355
+ }
356
+ return aliases.join('\n');
357
+ }
@@ -0,0 +1,23 @@
1
+ import type { CodecLookup } from '@prisma-next/framework-components/codec';
2
+ import type { TypesImportSpec } from '@prisma-next/framework-components/emission';
3
+
4
+ /**
5
+ * The subset of ControlStack that emit() reads.
6
+ * All fields are optional so tests can pass minimal objects.
7
+ * A full ControlStack satisfies this via structural typing.
8
+ */
9
+ export interface EmitStackInput {
10
+ readonly codecTypeImports?: ReadonlyArray<TypesImportSpec>;
11
+ readonly operationTypeImports?: ReadonlyArray<TypesImportSpec>;
12
+ readonly queryOperationTypeImports?: ReadonlyArray<TypesImportSpec>;
13
+ readonly extensionIds?: ReadonlyArray<string>;
14
+ readonly codecLookup?: CodecLookup;
15
+ }
16
+
17
+ export interface EmitResult {
18
+ readonly contractJson: string;
19
+ readonly contractDts: string;
20
+ readonly storageHash: string;
21
+ readonly executionHash?: string;
22
+ readonly profileHash: string;
23
+ }
package/src/emit.ts ADDED
@@ -0,0 +1,68 @@
1
+ import { canonicalizeContractToObject } from '@prisma-next/contract/hashing';
2
+ import type { Contract } from '@prisma-next/contract/types';
3
+ import type { EmissionSpi } from '@prisma-next/framework-components/emission';
4
+ import { ifDefined } from '@prisma-next/utils/defined';
5
+ import { format } from 'prettier';
6
+ import type { EmitResult, EmitStackInput } from './emit-types';
7
+ import { generateContractDts } from './generate-contract-dts';
8
+
9
+ const SCHEMA_VERSION = '1';
10
+
11
+ export async function emit(
12
+ contract: Contract,
13
+ stack: EmitStackInput,
14
+ targetFamily: EmissionSpi,
15
+ ): Promise<EmitResult> {
16
+ const { codecTypeImports, operationTypeImports, queryOperationTypeImports } = stack;
17
+
18
+ const { storageHash } = contract.storage;
19
+ const executionHash = contract.execution?.executionHash;
20
+ const { profileHash } = contract;
21
+
22
+ const canonicalized = canonicalizeContractToObject(contract, {
23
+ schemaVersion: SCHEMA_VERSION,
24
+ });
25
+ const contractJsonString = JSON.stringify(
26
+ {
27
+ ...canonicalized,
28
+ _generated: {
29
+ warning: '⚠️ GENERATED FILE - DO NOT EDIT',
30
+ message: 'This file is automatically generated by "prisma-next contract emit".',
31
+ regenerate: 'To regenerate, run: prisma-next contract emit',
32
+ },
33
+ },
34
+ null,
35
+ 2,
36
+ );
37
+
38
+ const generateOptions = queryOperationTypeImports ? { queryOperationTypeImports } : undefined;
39
+
40
+ const contractTypeHashes = {
41
+ storageHash,
42
+ ...ifDefined('executionHash', executionHash),
43
+ profileHash,
44
+ };
45
+ const contractDtsRaw = generateContractDts(
46
+ contract,
47
+ targetFamily,
48
+ codecTypeImports ?? [],
49
+ operationTypeImports ?? [],
50
+ contractTypeHashes,
51
+ generateOptions,
52
+ stack.codecLookup,
53
+ );
54
+ const contractDts = await format(contractDtsRaw, {
55
+ parser: 'typescript',
56
+ singleQuote: true,
57
+ semi: true,
58
+ printWidth: 100,
59
+ });
60
+
61
+ return {
62
+ contractJson: contractJsonString,
63
+ contractDts,
64
+ storageHash,
65
+ ...ifDefined('executionHash', executionHash),
66
+ profileHash,
67
+ };
68
+ }
@@ -1,15 +1,7 @@
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';
10
1
  export {
11
2
  deduplicateImports,
12
3
  generateCodecTypeIntersection,
4
+ generateFieldOutputTypesMap,
13
5
  generateHashTypeAliases,
14
6
  generateImportLines,
15
7
  generateModelRelationsType,
@@ -17,3 +9,6 @@ export {
17
9
  serializeObjectKey,
18
10
  serializeValue,
19
11
  } from '../domain-type-generation';
12
+ export { emit } from '../emit';
13
+ export type { EmitResult, EmitStackInput } from '../emit-types';
14
+ export { generateContractDts } from '../generate-contract-dts';