prisma-arktype 2.3.0 → 2.4.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 (3) hide show
  1. package/README.md +43 -0
  2. package/dist/index.js +201 -34
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -239,6 +239,7 @@ Control schema generation using annotations in your Prisma schema. All annotatio
239
239
  | `@prisma-arktype.input.hide` | Field | Hide from Create and Update input schemas |
240
240
  | `@prisma-arktype.create.input.hide` | Field | Hide from Create input schema only |
241
241
  | `@prisma-arktype.update.input.hide` | Field | Hide from Update input schema only |
242
+ | `@prisma-arktype.schema="<schema>"` | Field | Custom ArkType schema (inline or external) |
242
243
  | `@prisma-arktype.typeOverwrite="<type>"` | Field | Override the generated ArkType type |
243
244
 
244
245
  ### Hide Fields/Models
@@ -301,6 +302,48 @@ model User {
301
302
 
302
303
  This allows you to use any ArkType type definition, including built-in refinements like `string.email`, `string.url`, `number.integer`, etc.
303
304
 
305
+ ### Custom Schemas
306
+
307
+ Bring your own ArkType schemas for any field using `@prisma-arktype.schema`:
308
+
309
+ ```prisma
310
+ model User {
311
+ id String @id
312
+
313
+ /// Inline schema for structured JSON
314
+ /// @prisma-arktype.schema="{ name: 'string', age: 'number' }"
315
+ profile Json
316
+
317
+ /// External schema from a file (named export)
318
+ /// @prisma-arktype.schema="../schemas/address:AddressSchema"
319
+ address Json
320
+
321
+ /// External schema (default export)
322
+ /// @prisma-arktype.schema="../schemas/config"
323
+ settings Json
324
+ }
325
+ ```
326
+
327
+ **Import Path Rules:**
328
+ - Paths are relative to the generated validators directory
329
+ - Named exports use colon syntax: `"path:ExportName"`
330
+ - Default exports omit the colon: `"path"`
331
+ - Works with ANY field type (not just Json)
332
+
333
+ **Priority:** `schema` > `typeOverwrite` > default type mapping
334
+
335
+ **Example external schema file** (`schemas/address.ts`):
336
+ ```typescript
337
+ import { type } from "arktype";
338
+
339
+ export const AddressSchema = type({
340
+ street: "string",
341
+ city: "string",
342
+ zipCode: "string",
343
+ country: "string",
344
+ });
345
+ ```
346
+
304
347
  ## Type Mapping
305
348
 
306
349
  Prisma types are mapped to ArkType as follows:
package/dist/index.js CHANGED
@@ -43,6 +43,9 @@ function isHiddenInputUpdate(annotation) {
43
43
  function isTypeOverwrite(annotation) {
44
44
  return annotation.type === "TYPE_OVERWRITE";
45
45
  }
46
+ function isSchema(annotation) {
47
+ return annotation.type === "SCHEMA";
48
+ }
46
49
  const annotationKeys = [
47
50
  {
48
51
  keys: ["@prisma-arktype.hide", "@prisma-arktype.hidden"],
@@ -66,9 +69,11 @@ const annotationKeys = [
66
69
  ],
67
70
  type: "HIDDEN_INPUT_UPDATE"
68
71
  },
69
- { keys: ["@prisma-arktype.typeOverwrite"], type: "TYPE_OVERWRITE" }
72
+ { keys: ["@prisma-arktype.typeOverwrite"], type: "TYPE_OVERWRITE" },
73
+ { keys: ["@prisma-arktype.schema"], type: "SCHEMA" }
70
74
  ];
71
75
  const prismaArktypeTypeOverwriteRegex = /@prisma-arktype\.typeOverwrite=(.+)/;
76
+ const prismaArktypeSchemaRegex = /@prisma-arktype\.schema="([^"]+)"/;
72
77
  function extractAnnotations(documentation) {
73
78
  const annotations = [];
74
79
  const descriptionLines = [];
@@ -78,18 +83,52 @@ function extractAnnotations(documentation) {
78
83
  for (const { keys, type } of annotationKeys) {
79
84
  for (const key of keys) {
80
85
  if (line.includes(key)) {
81
- isAnnotation = true;
82
86
  if (type === "TYPE_OVERWRITE") {
83
87
  const match = line.match(prismaArktypeTypeOverwriteRegex);
84
88
  if (match && match[1]) {
89
+ isAnnotation = true;
85
90
  annotations.push({
86
91
  type: "TYPE_OVERWRITE",
87
92
  value: match[1].trim()
88
93
  });
89
- } else {
90
- throw new Error(`Invalid TYPE_OVERWRITE annotation: ${line}`);
94
+ }
95
+ } else if (type === "SCHEMA") {
96
+ const match = line.match(prismaArktypeSchemaRegex);
97
+ if (match && match[1]) {
98
+ isAnnotation = true;
99
+ const schemaValue = match[1].trim();
100
+ const isExternal = schemaValue.includes("/") || !(schemaValue.startsWith("{") || schemaValue.startsWith('"') || schemaValue.startsWith("'")) && schemaValue.includes(":") && // Ensure it's a module:export pattern, not object syntax like { key: value }
101
+ !schemaValue.includes(" ") && !schemaValue.includes(",");
102
+ if (isExternal) {
103
+ const colonIndex = schemaValue.indexOf(":");
104
+ if (colonIndex > 0) {
105
+ const path = schemaValue.substring(0, colonIndex).trim();
106
+ const exportName = schemaValue.substring(colonIndex + 1).trim();
107
+ annotations.push({
108
+ type: "SCHEMA",
109
+ value: schemaValue,
110
+ isExternal: true,
111
+ importPath: path,
112
+ exportName
113
+ });
114
+ } else {
115
+ annotations.push({
116
+ type: "SCHEMA",
117
+ value: schemaValue,
118
+ isExternal: true,
119
+ importPath: schemaValue
120
+ });
121
+ }
122
+ } else {
123
+ annotations.push({
124
+ type: "SCHEMA",
125
+ value: schemaValue,
126
+ isExternal: false
127
+ });
128
+ }
91
129
  }
92
130
  } else {
131
+ isAnnotation = true;
93
132
  annotations.push({ type });
94
133
  }
95
134
  break;
@@ -186,7 +225,8 @@ function processPlain(models) {
186
225
  processedPlain.push({
187
226
  name: model.name,
188
227
  stringified: result.stringified,
189
- enumDependencies: result.enumDependencies
228
+ enumDependencies: result.enumDependencies,
229
+ externalSchemaDependencies: result.externalSchemaDependencies
190
230
  });
191
231
  }
192
232
  }
@@ -202,6 +242,12 @@ function stringifyPlain(model, isInputCreate = false, isInputUpdate = false) {
202
242
  }
203
243
  const fields = [];
204
244
  const enumDependencies = [];
245
+ const externalSchemaDependencies = [];
246
+ function generateUniqueAlias(path, exportName, fieldName) {
247
+ const baseName = exportName || path.split("/").pop()?.replace(/\.(ts|js)$/, "") || "Schema";
248
+ const suffix = fieldName ? `_${fieldName}` : "";
249
+ return `${baseName}${suffix}`;
250
+ }
205
251
  for (const field of model.fields) {
206
252
  const {
207
253
  annotations: fieldAnnotations,
@@ -218,10 +264,36 @@ function stringifyPlain(model, isInputCreate = false, isInputUpdate = false) {
218
264
  if (config.ignoredKeysOnInputModels.includes(field.name)) continue;
219
265
  if (field.name.endsWith("Id") && field.relationName) continue;
220
266
  }
267
+ const schemaAnnotation = fieldAnnotations.find(isSchema);
221
268
  const typeOverwrite = fieldAnnotations.find(isTypeOverwrite);
222
269
  let fieldType;
223
270
  let fieldName = field.name;
224
- if (typeOverwrite) {
271
+ if (schemaAnnotation) {
272
+ if (schemaAnnotation.isExternal) {
273
+ const alias = generateUniqueAlias(
274
+ schemaAnnotation.importPath,
275
+ schemaAnnotation.exportName,
276
+ field.name
277
+ );
278
+ if (!externalSchemaDependencies.some((d) => d.localAlias === alias)) {
279
+ const dependency = {
280
+ importPath: schemaAnnotation.importPath,
281
+ localAlias: alias
282
+ };
283
+ if (schemaAnnotation.exportName) {
284
+ dependency.exportName = schemaAnnotation.exportName;
285
+ }
286
+ externalSchemaDependencies.push(dependency);
287
+ }
288
+ fieldType = alias;
289
+ } else {
290
+ if (schemaAnnotation.value.trim().startsWith("{")) {
291
+ fieldType = schemaAnnotation.value;
292
+ } else {
293
+ fieldType = `"${schemaAnnotation.value}"`;
294
+ }
295
+ }
296
+ } else if (typeOverwrite) {
225
297
  fieldType = typeOverwrite.value;
226
298
  } else if (isPrimitivePrismaFieldType(field.type)) {
227
299
  fieldType = stringifyPrimitiveType(field.type);
@@ -235,16 +307,17 @@ function stringifyPlain(model, isInputCreate = false, isInputUpdate = false) {
235
307
  } else {
236
308
  continue;
237
309
  }
238
- const isEnumType = field.kind === "enum" && !typeOverwrite;
310
+ const isEnumType = field.kind === "enum" && !typeOverwrite && !schemaAnnotation;
311
+ const isExternalSchema = schemaAnnotation?.isExternal === true;
239
312
  if (field.isList) {
240
- if (isEnumType) {
313
+ if (isExternalSchema || isEnumType) {
241
314
  fieldType = `${fieldType}.array()`;
242
315
  } else {
243
316
  fieldType = `"${wrapPrimitiveWithArray(fieldType.slice(1, -1))}"`;
244
317
  }
245
318
  }
246
319
  if (!field.isRequired) {
247
- if (isEnumType) {
320
+ if (isExternalSchema || isEnumType) {
248
321
  fieldType = `${fieldType}.or("null")`;
249
322
  } else {
250
323
  const inner = fieldType.slice(1, -1);
@@ -263,7 +336,8 @@ function stringifyPlain(model, isInputCreate = false, isInputUpdate = false) {
263
336
  stringified: `{
264
337
  ${fields.join(",\n ")}
265
338
  }`,
266
- enumDependencies
339
+ enumDependencies,
340
+ externalSchemaDependencies
267
341
  };
268
342
  }
269
343
  function stringifyPlainInputCreate(model) {
@@ -283,7 +357,8 @@ function processCreate(models) {
283
357
  processedCreate.push({
284
358
  name: model.name,
285
359
  stringified: result.stringified,
286
- enumDependencies: result.enumDependencies
360
+ enumDependencies: result.enumDependencies,
361
+ externalSchemaDependencies: result.externalSchemaDependencies
287
362
  });
288
363
  }
289
364
  }
@@ -532,9 +607,7 @@ function processSelect(models) {
532
607
  Object.freeze(processedSelect);
533
608
  }
534
609
  function stringifySelect(model) {
535
- const { hidden } = extractAnnotations(
536
- model.documentation
537
- );
610
+ const { hidden } = extractAnnotations(model.documentation);
538
611
  if (hidden) {
539
612
  return;
540
613
  }
@@ -560,7 +633,8 @@ function processUpdate(models) {
560
633
  processedUpdate.push({
561
634
  name: model.name,
562
635
  stringified: result.stringified,
563
- enumDependencies: result.enumDependencies
636
+ enumDependencies: result.enumDependencies,
637
+ externalSchemaDependencies: result.externalSchemaDependencies
564
638
  });
565
639
  }
566
640
  }
@@ -569,6 +643,7 @@ function processUpdate(models) {
569
643
 
570
644
  const processedWhere = [];
571
645
  const processedWhereUnique = [];
646
+ const extRegex = /\.(ts|js)$/;
572
647
  function processWhere(models) {
573
648
  for (const model of models) {
574
649
  const result = stringifyWhere(model);
@@ -576,7 +651,8 @@ function processWhere(models) {
576
651
  processedWhere.push({
577
652
  name: model.name,
578
653
  stringified: result.stringified,
579
- enumDependencies: result.enumDependencies
654
+ enumDependencies: result.enumDependencies,
655
+ externalSchemaDependencies: result.externalSchemaDependencies
580
656
  });
581
657
  }
582
658
  const uniqueResult = stringifyWhereUnique(model);
@@ -584,7 +660,8 @@ function processWhere(models) {
584
660
  processedWhereUnique.push({
585
661
  name: model.name,
586
662
  stringified: uniqueResult.stringified,
587
- enumDependencies: uniqueResult.enumDependencies
663
+ enumDependencies: uniqueResult.enumDependencies,
664
+ externalSchemaDependencies: uniqueResult.externalSchemaDependencies
588
665
  });
589
666
  }
590
667
  }
@@ -592,21 +669,51 @@ function processWhere(models) {
592
669
  Object.freeze(processedWhereUnique);
593
670
  }
594
671
  function stringifyWhere(model) {
595
- const { hidden } = extractAnnotations(
596
- model.documentation
597
- );
672
+ const { hidden } = extractAnnotations(model.documentation);
598
673
  if (hidden) {
599
674
  return;
600
675
  }
601
676
  const fields = [];
602
677
  const enumDependencies = [];
678
+ const externalSchemaDependencies = [];
679
+ function generateUniqueAlias(path, exportName, fieldName) {
680
+ const baseName = exportName || path.split("/").pop()?.replace(extRegex, "") || "Schema";
681
+ const suffix = fieldName ? `_${fieldName}` : "";
682
+ return `${baseName}${suffix}`;
683
+ }
603
684
  for (const field of model.fields) {
604
685
  const { annotations: fieldAnnotations, hidden: fieldHidden } = extractAnnotations(field.documentation);
605
686
  if (fieldHidden) continue;
606
687
  if (field.kind === "object") continue;
688
+ const schemaAnnotation = fieldAnnotations.find(isSchema);
607
689
  const typeOverwrite = fieldAnnotations.find(isTypeOverwrite);
608
690
  let fieldType;
609
- if (typeOverwrite) {
691
+ if (schemaAnnotation) {
692
+ if (schemaAnnotation.isExternal) {
693
+ const alias = generateUniqueAlias(
694
+ schemaAnnotation.importPath,
695
+ schemaAnnotation.exportName,
696
+ field.name
697
+ );
698
+ if (!externalSchemaDependencies.some((d) => d.localAlias === alias)) {
699
+ const dependency = {
700
+ importPath: schemaAnnotation.importPath,
701
+ localAlias: alias
702
+ };
703
+ if (schemaAnnotation.exportName) {
704
+ dependency.exportName = schemaAnnotation.exportName;
705
+ }
706
+ externalSchemaDependencies.push(dependency);
707
+ }
708
+ fieldType = alias;
709
+ } else {
710
+ if (schemaAnnotation.value.trim().startsWith("{")) {
711
+ fieldType = schemaAnnotation.value;
712
+ } else {
713
+ fieldType = `"${schemaAnnotation.value}"`;
714
+ }
715
+ }
716
+ } else if (typeOverwrite) {
610
717
  fieldType = typeOverwrite.value;
611
718
  } else if (isPrimitivePrismaFieldType(field.type)) {
612
719
  fieldType = stringifyPrimitiveType(field.type);
@@ -620,9 +727,10 @@ function stringifyWhere(model) {
620
727
  } else {
621
728
  continue;
622
729
  }
623
- const isEnumType = field.kind === "enum" && !typeOverwrite;
730
+ const isEnumType = field.kind === "enum" && !typeOverwrite && !schemaAnnotation;
731
+ const isExternalSchema = schemaAnnotation?.isExternal === true;
624
732
  if (field.isList) {
625
- if (isEnumType) {
733
+ if (isExternalSchema || isEnumType) {
626
734
  fieldType = `${fieldType}.array()`;
627
735
  } else {
628
736
  const inner = fieldType.slice(1, -1);
@@ -635,26 +743,57 @@ function stringifyWhere(model) {
635
743
  stringified: `{
636
744
  ${fields.join(",\n ")}
637
745
  }`,
638
- enumDependencies
746
+ enumDependencies,
747
+ externalSchemaDependencies
639
748
  };
640
749
  }
641
750
  function stringifyWhereUnique(model) {
642
- const { hidden } = extractAnnotations(
643
- model.documentation
644
- );
751
+ const { hidden } = extractAnnotations(model.documentation);
645
752
  if (hidden) {
646
753
  return;
647
754
  }
648
755
  const fields = [];
649
756
  const enumDependencies = [];
757
+ const externalSchemaDependencies = [];
758
+ function generateUniqueAlias(path, exportName, fieldName) {
759
+ const baseName = exportName || path.split("/").pop()?.replace(extRegex, "") || "Schema";
760
+ const suffix = fieldName ? `_${fieldName}` : "";
761
+ return `${baseName}${suffix}`;
762
+ }
650
763
  for (const field of model.fields) {
651
764
  const { annotations: fieldAnnotations, hidden: fieldHidden } = extractAnnotations(field.documentation);
652
765
  if (fieldHidden) continue;
653
766
  if (field.kind === "object") continue;
654
767
  if (!(field.isId || field.isUnique)) continue;
768
+ const schemaAnnotation = fieldAnnotations.find(isSchema);
655
769
  const typeOverwrite = fieldAnnotations.find(isTypeOverwrite);
656
770
  let fieldType;
657
- if (typeOverwrite) {
771
+ if (schemaAnnotation) {
772
+ if (schemaAnnotation.isExternal) {
773
+ const alias = generateUniqueAlias(
774
+ schemaAnnotation.importPath,
775
+ schemaAnnotation.exportName,
776
+ field.name
777
+ );
778
+ if (!externalSchemaDependencies.some((d) => d.localAlias === alias)) {
779
+ const dependency = {
780
+ importPath: schemaAnnotation.importPath,
781
+ localAlias: alias
782
+ };
783
+ if (schemaAnnotation.exportName) {
784
+ dependency.exportName = schemaAnnotation.exportName;
785
+ }
786
+ externalSchemaDependencies.push(dependency);
787
+ }
788
+ fieldType = alias;
789
+ } else {
790
+ if (schemaAnnotation.value.trim().startsWith("{")) {
791
+ fieldType = schemaAnnotation.value;
792
+ } else {
793
+ fieldType = `"${schemaAnnotation.value}"`;
794
+ }
795
+ }
796
+ } else if (typeOverwrite) {
658
797
  fieldType = typeOverwrite.value;
659
798
  } else if (isPrimitivePrismaFieldType(field.type)) {
660
799
  fieldType = stringifyPrimitiveType(field.type);
@@ -677,7 +816,8 @@ function stringifyWhereUnique(model) {
677
816
  stringified: `{
678
817
  ${fields.join(",\n ")}
679
818
  }`,
680
- enumDependencies
819
+ enumDependencies,
820
+ externalSchemaDependencies
681
821
  };
682
822
  }
683
823
 
@@ -700,6 +840,18 @@ function generateModelImports(modelDependencies) {
700
840
  ).join("\n")}
701
841
  `;
702
842
  }
843
+ function generateExternalSchemaImports(externalSchemaDependencies) {
844
+ if (!externalSchemaDependencies || externalSchemaDependencies.length === 0) {
845
+ return "";
846
+ }
847
+ return `${externalSchemaDependencies.map((dep) => {
848
+ if (dep.exportName) {
849
+ return `import { ${dep.exportName} as ${dep.localAlias} } from "${dep.importPath}";`;
850
+ }
851
+ return `import ${dep.localAlias} from "${dep.importPath}";`;
852
+ }).join("\n")}
853
+ `;
854
+ }
703
855
  function mapAllModelsForWrite(processedEnums, processedPlain, processedRelations, processedWhere, processedWhereUnique, processedCreate, processedUpdate, processedRelationsCreate, processedRelationsUpdate, processedSelect, processedInclude, processedOrderBy) {
704
856
  const config = getConfig();
705
857
  const modelMap = /* @__PURE__ */ new Map();
@@ -713,7 +865,10 @@ function mapAllModelsForWrite(processedEnums, processedPlain, processedRelations
713
865
  }
714
866
  for (const model of processedPlain) {
715
867
  const enumImports = generateEnumImports(model.enumDependencies);
716
- const content = `${arktypeImport}${enumImports}export const ${model.name}Plain = type(${model.stringified});
868
+ const externalSchemaImports = generateExternalSchemaImports(
869
+ model.externalSchemaDependencies
870
+ );
871
+ const content = `${arktypeImport}${enumImports}${externalSchemaImports}export const ${model.name}Plain = type(${model.stringified});
717
872
  `;
718
873
  modelMap.set(`${model.name}Plain`, content);
719
874
  }
@@ -742,25 +897,37 @@ export const ${plain.name} = ${plain.name}Plain;
742
897
  }
743
898
  for (const model of processedWhere) {
744
899
  const enumImports = generateEnumImports(model.enumDependencies);
745
- const content = `${arktypeImport}${enumImports}export const ${model.name}Where = type(${model.stringified});
900
+ const externalSchemaImports = generateExternalSchemaImports(
901
+ model.externalSchemaDependencies
902
+ );
903
+ const content = `${arktypeImport}${enumImports}${externalSchemaImports}export const ${model.name}Where = type(${model.stringified});
746
904
  `;
747
905
  modelMap.set(`${model.name}Where`, content);
748
906
  }
749
907
  for (const model of processedWhereUnique) {
750
908
  const enumImports = generateEnumImports(model.enumDependencies);
751
- const content = `${arktypeImport}${enumImports}export const ${model.name}WhereUnique = type(${model.stringified});
909
+ const externalSchemaImports = generateExternalSchemaImports(
910
+ model.externalSchemaDependencies
911
+ );
912
+ const content = `${arktypeImport}${enumImports}${externalSchemaImports}export const ${model.name}WhereUnique = type(${model.stringified});
752
913
  `;
753
914
  modelMap.set(`${model.name}WhereUnique`, content);
754
915
  }
755
916
  for (const model of processedCreate) {
756
917
  const enumImports = generateEnumImports(model.enumDependencies);
757
- const content = `${arktypeImport}${enumImports}export const ${model.name}Create = type(${model.stringified});
918
+ const externalSchemaImports = generateExternalSchemaImports(
919
+ model.externalSchemaDependencies
920
+ );
921
+ const content = `${arktypeImport}${enumImports}${externalSchemaImports}export const ${model.name}Create = type(${model.stringified});
758
922
  `;
759
923
  modelMap.set(`${model.name}Create`, content);
760
924
  }
761
925
  for (const model of processedUpdate) {
762
926
  const enumImports = generateEnumImports(model.enumDependencies);
763
- const content = `${arktypeImport}${enumImports}export const ${model.name}Update = type(${model.stringified});
927
+ const externalSchemaImports = generateExternalSchemaImports(
928
+ model.externalSchemaDependencies
929
+ );
930
+ const content = `${arktypeImport}${enumImports}${externalSchemaImports}export const ${model.name}Update = type(${model.stringified});
764
931
  `;
765
932
  modelMap.set(`${model.name}Update`, content);
766
933
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prisma-arktype",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Generate ArkType schemas from your Prisma schema",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -55,11 +55,11 @@
55
55
  "vitest": "^4.0.16"
56
56
  },
57
57
  "scripts": {
58
- "build": "pkgroll",
58
+ "build": "pkgroll && prisma generate",
59
59
  "dev": "pkgroll --watch",
60
60
  "test": "vitest run",
61
61
  "test:watch": "vitest",
62
- "test:e2e": "pnpm build && prisma generate && vitest run",
62
+ "test:e2e": "pnpm build && vitest run",
63
63
  "pretest": "pnpm build",
64
64
  "postinstall": "lefthook install",
65
65
  "lint": "biome check",