@drzl/validation-core 1.1.0 → 2.0.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.
package/README.md CHANGED
@@ -37,3 +37,23 @@ Shared interfaces and helpers used by validation generators.
37
37
  - Helpers
38
38
  - `insertColumns(table)`, `updateColumns(table)`, `selectColumns(table)`
39
39
  - `formatCode(code, filePath, formatOpts)`
40
+ - File names (shared by every generator that writes a file and a barrel)
41
+ - `moduleFileName(tsName, fileSuffix)` -> `users.zod.ts`
42
+ - `moduleSpecifier(tsName, fileSuffix, importExtension?)` -> `./users.zod.js`, the
43
+ specifier a sibling module needs to import that file.
44
+ - `importSpecifier(relativePath, importExtension?)` does the same for a path a generator
45
+ already has in hand, e.g. `./types/users.ts` -> `./types/users.js`.
46
+ - Both read the same `fileSuffix`, so a barrel can never name a file that was not written.
47
+ - `ImportExtension` is `'js' | 'none' | 'ts'`, defaulting to `DEFAULT_IMPORT_EXTENSION`
48
+ (`'js'`). `'js'` is the only form that resolves under `bundler`, `node10`, `node16` and
49
+ `nodenext`, in both CommonJS and ESM, with no compiler flag. `'none'` is the pre-2.0
50
+ output and misses `node16`/`nodenext` ES modules. `'ts'` needs
51
+ `allowImportingTsExtensions`. `.mts` and `.cts` become `.mjs` and `.cjs` under both
52
+ `'js'` and `'none'`, since an extensionless specifier never resolves to them.
53
+ - Naming (shared by every generator that emits a schema name)
54
+ - `resolveAffix({ affix, schemaSuffix })` -> `ResolvedAffix`
55
+ - `schemaName(mode, tsName, resolved)`, `typeName(mode, tsName, resolved)`
56
+ - `validateAffix(affix, schemaSuffix)` -> issues for unusable or colliding names
57
+ - `pascalCase(s)`, `applyTableCase(tsName, 'preserve' | 'pascal')`
58
+ - Calling `resolveAffix()` with no options reproduces the original naming exactly, so the
59
+ generators and the oRPC router can never interpret one config two ways.
package/dist/index.cjs CHANGED
@@ -100171,13 +100171,162 @@ ${codeblock}`, options8);
100171
100171
  // src/index.ts
100172
100172
  var index_exports2 = {};
100173
100173
  __export(index_exports2, {
100174
+ AFFIX_PROBE_TABLE: () => AFFIX_PROBE_TABLE,
100175
+ DEFAULT_IMPORT_EXTENSION: () => DEFAULT_IMPORT_EXTENSION,
100176
+ DEFAULT_MODE_PREFIX: () => DEFAULT_MODE_PREFIX,
100177
+ DEFAULT_SCHEMA_SUFFIX: () => DEFAULT_SCHEMA_SUFFIX,
100178
+ DEFAULT_TYPE_SUFFIX: () => DEFAULT_TYPE_SUFFIX,
100179
+ IMPORT_EXTENSIONS: () => IMPORT_EXTENSIONS,
100180
+ NAME_MODES: () => NAME_MODES,
100181
+ applyTableCase: () => applyTableCase,
100174
100182
  formatCode: () => formatCode,
100183
+ importSpecifier: () => importSpecifier,
100175
100184
  insertColumns: () => insertColumns,
100176
100185
  isGeneratedColumn: () => isGeneratedColumn,
100186
+ moduleFileName: () => moduleFileName,
100187
+ moduleSpecifier: () => moduleSpecifier,
100188
+ pascalCase: () => pascalCase,
100189
+ resolveAffix: () => resolveAffix,
100190
+ schemaName: () => schemaName,
100177
100191
  selectColumns: () => selectColumns,
100178
- updateColumns: () => updateColumns
100192
+ typeName: () => typeName,
100193
+ updateColumns: () => updateColumns,
100194
+ validateAffix: () => validateAffix
100179
100195
  });
100180
100196
  module.exports = __toCommonJS(index_exports2);
100197
+
100198
+ // src/files.ts
100199
+ var IMPORT_EXTENSIONS = ["js", "none", "ts"];
100200
+ var DEFAULT_IMPORT_EXTENSION = "js";
100201
+ var TS_EXTENSIONS = [
100202
+ { ext: ".mts", js: ".mjs", none: ".mjs" },
100203
+ { ext: ".cts", js: ".cjs", none: ".cjs" },
100204
+ { ext: ".tsx", js: ".js", none: "" },
100205
+ { ext: ".ts", js: ".js", none: "" }
100206
+ ];
100207
+ function moduleFileName(tsName, fileSuffix) {
100208
+ return `${tsName}${fileSuffix}`;
100209
+ }
100210
+ function importSpecifier(relativePath, importExtension = DEFAULT_IMPORT_EXTENSION) {
100211
+ for (const { ext, js: js8, none } of TS_EXTENSIONS) {
100212
+ if (!relativePath.endsWith(ext)) continue;
100213
+ const stem = relativePath.slice(0, -ext.length);
100214
+ if (importExtension === "ts") return relativePath;
100215
+ return `${stem}${importExtension === "none" ? none : js8}`;
100216
+ }
100217
+ return relativePath;
100218
+ }
100219
+ function moduleSpecifier(tsName, fileSuffix, importExtension = DEFAULT_IMPORT_EXTENSION) {
100220
+ return importSpecifier(`./${moduleFileName(tsName, fileSuffix)}`, importExtension);
100221
+ }
100222
+
100223
+ // src/naming.ts
100224
+ var NAME_MODES = ["insert", "update", "select"];
100225
+ var DEFAULT_MODE_PREFIX = {
100226
+ insert: "Insert",
100227
+ update: "Update",
100228
+ select: "Select"
100229
+ };
100230
+ var DEFAULT_TYPE_SUFFIX = {
100231
+ insert: "Input",
100232
+ update: "Input",
100233
+ select: "Output"
100234
+ };
100235
+ var DEFAULT_SCHEMA_SUFFIX = "Schema";
100236
+ var AFFIX_PROBE_TABLE = "users";
100237
+ function spread(value, fallback) {
100238
+ if (value === void 0) return { ...fallback };
100239
+ if (typeof value === "string") return { insert: value, update: value, select: value };
100240
+ return {
100241
+ insert: value.insert ?? fallback.insert,
100242
+ update: value.update ?? fallback.update,
100243
+ select: value.select ?? fallback.select
100244
+ };
100245
+ }
100246
+ function pascalCase(s) {
100247
+ return s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[\s_-]+/).filter(Boolean).map((p5) => p5.charAt(0).toUpperCase() + p5.slice(1)).join("");
100248
+ }
100249
+ function applyTableCase(tsName, tableCase) {
100250
+ return tableCase === "pascal" ? pascalCase(tsName) : tsName;
100251
+ }
100252
+ function resolveAffix(opts) {
100253
+ const affix = opts?.affix;
100254
+ const legacy = opts?.schemaSuffix ?? DEFAULT_SCHEMA_SUFFIX;
100255
+ const legacyMap = {
100256
+ insert: legacy,
100257
+ update: legacy,
100258
+ select: legacy
100259
+ };
100260
+ return {
100261
+ tableCase: affix?.tableCase ?? "preserve",
100262
+ schema: {
100263
+ prefix: spread(affix?.schema?.prefix, DEFAULT_MODE_PREFIX),
100264
+ suffix: spread(affix?.schema?.suffix, legacyMap)
100265
+ },
100266
+ type: {
100267
+ prefix: spread(affix?.type?.prefix, DEFAULT_MODE_PREFIX),
100268
+ suffix: spread(affix?.type?.suffix, DEFAULT_TYPE_SUFFIX)
100269
+ }
100270
+ };
100271
+ }
100272
+ function schemaName(mode, tsName, affix) {
100273
+ return affix.schema.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.schema.suffix[mode];
100274
+ }
100275
+ function typeName(mode, tsName, affix) {
100276
+ return affix.type.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.type.suffix[mode];
100277
+ }
100278
+ var PREFIX_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
100279
+ var SUFFIX_RE = /^[A-Za-z0-9_$]+$/;
100280
+ function validateAffix(affix, schemaSuffix) {
100281
+ const issues = [];
100282
+ if (!affix) return issues;
100283
+ const checkOne = (value, path14, kind) => {
100284
+ if (value === "") return;
100285
+ const ok = kind === "prefix" ? PREFIX_RE.test(value) : SUFFIX_RE.test(value);
100286
+ if (ok) return;
100287
+ issues.push({
100288
+ path: path14,
100289
+ message: `${JSON.stringify(value)} cannot appear in a TypeScript identifier. Use only letters, digits, "_" and "$"` + (kind === "prefix" ? ", and do not start with a digit." : ".")
100290
+ });
100291
+ };
100292
+ const checkValue = (value, base, kind) => {
100293
+ if (value === void 0) return;
100294
+ if (typeof value === "string") {
100295
+ checkOne(value, base, kind);
100296
+ return;
100297
+ }
100298
+ for (const mode of NAME_MODES) {
100299
+ const v7 = value[mode];
100300
+ if (v7 !== void 0) checkOne(v7, [...base, mode], kind);
100301
+ }
100302
+ };
100303
+ checkValue(affix.schema?.prefix, ["schema", "prefix"], "prefix");
100304
+ checkValue(affix.schema?.suffix, ["schema", "suffix"], "suffix");
100305
+ checkValue(affix.type?.prefix, ["type", "prefix"], "prefix");
100306
+ checkValue(affix.type?.suffix, ["type", "suffix"], "suffix");
100307
+ if (issues.length) return issues;
100308
+ const resolved = resolveAffix({ affix, schemaSuffix });
100309
+ const collisions = (space, build) => {
100310
+ const seen = /* @__PURE__ */ new Map();
100311
+ for (const mode of NAME_MODES) {
100312
+ const name = build(mode);
100313
+ const first = seen.get(name);
100314
+ if (first) {
100315
+ issues.push({
100316
+ path: [space],
100317
+ message: `The ${space} names for "${first}" and "${mode}" collide: both resolve to "${name}". All three are emitted into the same file, so at least one prefix or suffix has to differ.`
100318
+ });
100319
+ } else {
100320
+ seen.set(name, mode);
100321
+ }
100322
+ }
100323
+ };
100324
+ collisions("schema", (mode) => schemaName(mode, AFFIX_PROBE_TABLE, resolved));
100325
+ collisions("type", (mode) => typeName(mode, AFFIX_PROBE_TABLE, resolved));
100326
+ return issues;
100327
+ }
100328
+
100329
+ // src/index.ts
100181
100330
  function isGeneratedColumn(c5, primaryKeyColumns) {
100182
100331
  return c5.isGenerated || primaryKeyColumns.includes(c5.name);
100183
100332
  }
@@ -100219,9 +100368,25 @@ async function formatCode(code, filePath, fmt) {
100219
100368
  }
100220
100369
  // Annotate the CommonJS export names for ESM import in node:
100221
100370
  0 && (module.exports = {
100371
+ AFFIX_PROBE_TABLE,
100372
+ DEFAULT_IMPORT_EXTENSION,
100373
+ DEFAULT_MODE_PREFIX,
100374
+ DEFAULT_SCHEMA_SUFFIX,
100375
+ DEFAULT_TYPE_SUFFIX,
100376
+ IMPORT_EXTENSIONS,
100377
+ NAME_MODES,
100378
+ applyTableCase,
100222
100379
  formatCode,
100380
+ importSpecifier,
100223
100381
  insertColumns,
100224
100382
  isGeneratedColumn,
100383
+ moduleFileName,
100384
+ moduleSpecifier,
100385
+ pascalCase,
100386
+ resolveAffix,
100387
+ schemaName,
100225
100388
  selectColumns,
100226
- updateColumns
100389
+ typeName,
100390
+ updateColumns,
100391
+ validateAffix
100227
100392
  });
package/dist/index.d.cts CHANGED
@@ -1,5 +1,150 @@
1
1
  import { Column, Analysis } from '@drzl/analyzer';
2
2
 
3
+ /**
4
+ * One place that decides what a generated module is called, on disk and in an import.
5
+ *
6
+ * The three validation generators used to name the file from `fileSuffix` but hardcode the
7
+ * default suffix in the barrel, so any custom `fileSuffix` produced `export * from
8
+ * './users.zod'` next to a file called `users.schema.ts` and the consumer's build failed on
9
+ * an unresolved import. Both halves are derived here from the same value now.
10
+ */
11
+ /**
12
+ * How a relative import of a generated file spells its extension.
13
+ *
14
+ * Generated files land in the consumer's own source tree, so the consumer's
15
+ * `moduleResolution` decides which forms resolve. Measured against tsc 5.9.2 and 7.0.2, for
16
+ * a specifier pointing at a sibling `.ts` file:
17
+ *
18
+ * | form | bundler | node10 | node16 / nodenext (CJS) | node16 / nodenext (ESM) |
19
+ * | --------------- | ------- | ------ | ----------------------- | ----------------------- |
20
+ * | `'js'` | yes | yes | yes | yes |
21
+ * | `'none'` | yes | yes | yes | **no** |
22
+ * | `'ts'` | flag | flag | flag | flag |
23
+ *
24
+ * `'js'` is the default because it is the only form that needs no compiler flag and still
25
+ * resolves in every cell. `'none'` is what drzl emitted before 2.0 and is what a pipeline
26
+ * that cannot map `.js` back to `.ts` wants (webpack without `resolve.extensionAlias`,
27
+ * ts-jest without a `moduleNameMapper`). `'ts'` needs `allowImportingTsExtensions`, and is
28
+ * the only form Node's own type stripping accepts, so it suits a project that runs the
29
+ * generated `.ts` unbuilt.
30
+ */
31
+ type ImportExtension = (typeof IMPORT_EXTENSIONS)[number];
32
+ /**
33
+ * Every value `ImportExtension` accepts, in the order documentation lists them. The type is
34
+ * derived from this tuple rather than declared alongside it, so a config schema built from
35
+ * it accepts exactly what the type allows and the two cannot drift.
36
+ */
37
+ declare const IMPORT_EXTENSIONS: readonly ["js", "none", "ts"];
38
+ /** What `importExtension` means when nothing sets it. */
39
+ declare const DEFAULT_IMPORT_EXTENSION: ImportExtension;
40
+ /** Name of the file a table is written to, e.g. `users.zod.ts`. */
41
+ declare function moduleFileName(tsName: string, fileSuffix: string): string;
42
+ /**
43
+ * Rewrite the extension of a relative path naming a generated file into the form an import
44
+ * specifier has to spell, e.g. `./users.zod.ts` -> `./users.zod.js`.
45
+ *
46
+ * A path that ends in no TypeScript extension is left whole: such a file cannot be imported
47
+ * at all, and naming a neighbour that does not exist would only hide that.
48
+ */
49
+ declare function importSpecifier(relativePath: string, importExtension?: ImportExtension): string;
50
+ /**
51
+ * Relative specifier a sibling module needs to import a table's file, e.g.
52
+ * `./users.zod.js`.
53
+ */
54
+ declare function moduleSpecifier(tsName: string, fileSuffix: string, importExtension?: ImportExtension): string;
55
+
56
+ /**
57
+ * One place that decides what a generated identifier is called.
58
+ *
59
+ * Every generator used to build these names by hand with template literals, which is why
60
+ * the oRPC router and the zod/valibot/arktype generators could silently disagree about the
61
+ * same name. They all call through here now, so a single resolved value describes both
62
+ * sides of the import.
63
+ *
64
+ * Defaults reproduce the pre-affix output byte for byte:
65
+ * schema: Insert|Update|Select + <tsName> + (schemaSuffix ?? 'Schema')
66
+ * type: Insert|Update|Select + <tsName> + Input|Input|Output
67
+ */
68
+ type NameMode = 'insert' | 'update' | 'select';
69
+ /** How the Drizzle export name is cased before it goes into an identifier. */
70
+ type TableCase = 'preserve' | 'pascal';
71
+ /** One affix for all three modes, or a per-mode override map. */
72
+ type AffixValue = string | Partial<Record<NameMode, string>>;
73
+ interface AffixOptions {
74
+ /**
75
+ * `preserve` (default) interpolates the Drizzle export name verbatim, which is what every
76
+ * released version does: `export const users` yields `InsertusersSchema`. `pascal` upper-camels
77
+ * it first, yielding `InsertUsersSchema`. Identifiers only; file names are never re-cased.
78
+ */
79
+ tableCase?: TableCase;
80
+ /** Affixes for the exported schema constants. */
81
+ schema?: {
82
+ prefix?: AffixValue;
83
+ suffix?: AffixValue;
84
+ };
85
+ /** Affixes for the exported type aliases. Independent of `schema`. */
86
+ type?: {
87
+ prefix?: AffixValue;
88
+ suffix?: AffixValue;
89
+ };
90
+ }
91
+ interface ResolvedAffix {
92
+ tableCase: TableCase;
93
+ schema: {
94
+ prefix: Record<NameMode, string>;
95
+ suffix: Record<NameMode, string>;
96
+ };
97
+ type: {
98
+ prefix: Record<NameMode, string>;
99
+ suffix: Record<NameMode, string>;
100
+ };
101
+ }
102
+ interface AffixIssue {
103
+ /** Path relative to the affix object, e.g. `['schema', 'suffix']`. */
104
+ path: (string | number)[];
105
+ message: string;
106
+ }
107
+ declare const NAME_MODES: readonly NameMode[];
108
+ declare const DEFAULT_MODE_PREFIX: Readonly<Record<NameMode, string>>;
109
+ declare const DEFAULT_TYPE_SUFFIX: Readonly<Record<NameMode, string>>;
110
+ declare const DEFAULT_SCHEMA_SUFFIX = "Schema";
111
+ /** Table name used when a config is checked for invalid or colliding names. */
112
+ declare const AFFIX_PROBE_TABLE = "users";
113
+ /**
114
+ * Real PascalCase, unlike the `cap()` helpers scattered around the repo which only upcase
115
+ * character zero. Splits on `_`, `-`, whitespace and camel boundaries, and leaves the rest of
116
+ * each part alone so acronyms survive (`userID` -> `UserID`, not `Userid`).
117
+ */
118
+ declare function pascalCase(s: string): string;
119
+ declare function applyTableCase(tsName: string, tableCase: TableCase): string;
120
+ /**
121
+ * Fold an `affix` block and the legacy flat `schemaSuffix` into one fully-populated value.
122
+ * `affix.schema.suffix` wins over `schemaSuffix`; `schemaSuffix` wins over the built-in
123
+ * `'Schema'`. Calling with no arguments returns exactly today's naming.
124
+ */
125
+ declare function resolveAffix(opts?: {
126
+ affix?: AffixOptions;
127
+ schemaSuffix?: string;
128
+ }): ResolvedAffix;
129
+ /** Name of the exported schema constant, e.g. `InsertusersSchema`. */
130
+ declare function schemaName(mode: NameMode, tsName: string, affix: ResolvedAffix): string;
131
+ /** Name of the exported type alias, e.g. `InsertusersInput`. */
132
+ declare function typeName(mode: NameMode, tsName: string, affix: ResolvedAffix): string;
133
+ /**
134
+ * Reject affixes that cannot produce a compilable file, before anything is written:
135
+ * - characters that are not legal in a TypeScript identifier
136
+ * - two names in the same declaration space resolving to the same string
137
+ *
138
+ * A schema name equal to a type name is allowed on purpose: `export const X` and
139
+ * `export type X` occupy different declaration spaces, and the generators already emit
140
+ * `type ... = z.input<typeof ...>` pairs.
141
+ *
142
+ * Only what the caller actually wrote in `affix` is checked. The legacy flat `schemaSuffix`
143
+ * is not character-checked, because it never was, and rejecting it now would break configs
144
+ * that parse today.
145
+ */
146
+ declare function validateAffix(affix?: AffixOptions, schemaSuffix?: string): AffixIssue[];
147
+
3
148
  interface Table {
4
149
  name: string;
5
150
  tsName: string;
@@ -17,8 +162,26 @@ interface FormatOptions {
17
162
  interface ValidationGenerateOptions {
18
163
  outDir: string;
19
164
  format?: FormatOptions;
165
+ /**
166
+ * What every generated file is called after the Drizzle export name, e.g. `.zod.ts`
167
+ * yields `users.zod.ts`. The barrel derives its import specifiers from this same value,
168
+ * so a custom suffix keeps resolving.
169
+ */
20
170
  fileSuffix?: string;
171
+ /**
172
+ * How the barrel spells the extension of the files it re-exports. Defaults to `'js'`,
173
+ * so `users.zod.ts` is imported as `./users.zod.js`, the only form that resolves under
174
+ * every `moduleResolution` without a compiler flag. Use `'none'` for the extensionless
175
+ * specifiers drzl emitted before 2.0.
176
+ */
177
+ importExtension?: ImportExtension;
21
178
  schemaSuffix?: string;
179
+ /**
180
+ * Prefixes, suffixes and table casing for the generated identifiers. Omit it and the
181
+ * output is identical to every previous version; `schemaSuffix` stays the fallback for
182
+ * `affix.schema.suffix`.
183
+ */
184
+ affix?: AffixOptions;
22
185
  coerceDates?: 'input' | 'all' | 'none';
23
186
  emit?: {
24
187
  select?: boolean;
@@ -38,4 +201,4 @@ declare function updateColumns(table: Table): Column[];
38
201
  declare function selectColumns(table: Table): Column[];
39
202
  declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
40
203
 
41
- export { type FormatOptions, type Table, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, formatCode, insertColumns, isGeneratedColumn, selectColumns, updateColumns };
204
+ export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, NAME_MODES, type NameMode, type ResolvedAffix, type Table, type TableCase, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, formatCode, importSpecifier, insertColumns, isGeneratedColumn, moduleFileName, moduleSpecifier, pascalCase, resolveAffix, schemaName, selectColumns, typeName, updateColumns, validateAffix };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,150 @@
1
1
  import { Column, Analysis } from '@drzl/analyzer';
2
2
 
3
+ /**
4
+ * One place that decides what a generated module is called, on disk and in an import.
5
+ *
6
+ * The three validation generators used to name the file from `fileSuffix` but hardcode the
7
+ * default suffix in the barrel, so any custom `fileSuffix` produced `export * from
8
+ * './users.zod'` next to a file called `users.schema.ts` and the consumer's build failed on
9
+ * an unresolved import. Both halves are derived here from the same value now.
10
+ */
11
+ /**
12
+ * How a relative import of a generated file spells its extension.
13
+ *
14
+ * Generated files land in the consumer's own source tree, so the consumer's
15
+ * `moduleResolution` decides which forms resolve. Measured against tsc 5.9.2 and 7.0.2, for
16
+ * a specifier pointing at a sibling `.ts` file:
17
+ *
18
+ * | form | bundler | node10 | node16 / nodenext (CJS) | node16 / nodenext (ESM) |
19
+ * | --------------- | ------- | ------ | ----------------------- | ----------------------- |
20
+ * | `'js'` | yes | yes | yes | yes |
21
+ * | `'none'` | yes | yes | yes | **no** |
22
+ * | `'ts'` | flag | flag | flag | flag |
23
+ *
24
+ * `'js'` is the default because it is the only form that needs no compiler flag and still
25
+ * resolves in every cell. `'none'` is what drzl emitted before 2.0 and is what a pipeline
26
+ * that cannot map `.js` back to `.ts` wants (webpack without `resolve.extensionAlias`,
27
+ * ts-jest without a `moduleNameMapper`). `'ts'` needs `allowImportingTsExtensions`, and is
28
+ * the only form Node's own type stripping accepts, so it suits a project that runs the
29
+ * generated `.ts` unbuilt.
30
+ */
31
+ type ImportExtension = (typeof IMPORT_EXTENSIONS)[number];
32
+ /**
33
+ * Every value `ImportExtension` accepts, in the order documentation lists them. The type is
34
+ * derived from this tuple rather than declared alongside it, so a config schema built from
35
+ * it accepts exactly what the type allows and the two cannot drift.
36
+ */
37
+ declare const IMPORT_EXTENSIONS: readonly ["js", "none", "ts"];
38
+ /** What `importExtension` means when nothing sets it. */
39
+ declare const DEFAULT_IMPORT_EXTENSION: ImportExtension;
40
+ /** Name of the file a table is written to, e.g. `users.zod.ts`. */
41
+ declare function moduleFileName(tsName: string, fileSuffix: string): string;
42
+ /**
43
+ * Rewrite the extension of a relative path naming a generated file into the form an import
44
+ * specifier has to spell, e.g. `./users.zod.ts` -> `./users.zod.js`.
45
+ *
46
+ * A path that ends in no TypeScript extension is left whole: such a file cannot be imported
47
+ * at all, and naming a neighbour that does not exist would only hide that.
48
+ */
49
+ declare function importSpecifier(relativePath: string, importExtension?: ImportExtension): string;
50
+ /**
51
+ * Relative specifier a sibling module needs to import a table's file, e.g.
52
+ * `./users.zod.js`.
53
+ */
54
+ declare function moduleSpecifier(tsName: string, fileSuffix: string, importExtension?: ImportExtension): string;
55
+
56
+ /**
57
+ * One place that decides what a generated identifier is called.
58
+ *
59
+ * Every generator used to build these names by hand with template literals, which is why
60
+ * the oRPC router and the zod/valibot/arktype generators could silently disagree about the
61
+ * same name. They all call through here now, so a single resolved value describes both
62
+ * sides of the import.
63
+ *
64
+ * Defaults reproduce the pre-affix output byte for byte:
65
+ * schema: Insert|Update|Select + <tsName> + (schemaSuffix ?? 'Schema')
66
+ * type: Insert|Update|Select + <tsName> + Input|Input|Output
67
+ */
68
+ type NameMode = 'insert' | 'update' | 'select';
69
+ /** How the Drizzle export name is cased before it goes into an identifier. */
70
+ type TableCase = 'preserve' | 'pascal';
71
+ /** One affix for all three modes, or a per-mode override map. */
72
+ type AffixValue = string | Partial<Record<NameMode, string>>;
73
+ interface AffixOptions {
74
+ /**
75
+ * `preserve` (default) interpolates the Drizzle export name verbatim, which is what every
76
+ * released version does: `export const users` yields `InsertusersSchema`. `pascal` upper-camels
77
+ * it first, yielding `InsertUsersSchema`. Identifiers only; file names are never re-cased.
78
+ */
79
+ tableCase?: TableCase;
80
+ /** Affixes for the exported schema constants. */
81
+ schema?: {
82
+ prefix?: AffixValue;
83
+ suffix?: AffixValue;
84
+ };
85
+ /** Affixes for the exported type aliases. Independent of `schema`. */
86
+ type?: {
87
+ prefix?: AffixValue;
88
+ suffix?: AffixValue;
89
+ };
90
+ }
91
+ interface ResolvedAffix {
92
+ tableCase: TableCase;
93
+ schema: {
94
+ prefix: Record<NameMode, string>;
95
+ suffix: Record<NameMode, string>;
96
+ };
97
+ type: {
98
+ prefix: Record<NameMode, string>;
99
+ suffix: Record<NameMode, string>;
100
+ };
101
+ }
102
+ interface AffixIssue {
103
+ /** Path relative to the affix object, e.g. `['schema', 'suffix']`. */
104
+ path: (string | number)[];
105
+ message: string;
106
+ }
107
+ declare const NAME_MODES: readonly NameMode[];
108
+ declare const DEFAULT_MODE_PREFIX: Readonly<Record<NameMode, string>>;
109
+ declare const DEFAULT_TYPE_SUFFIX: Readonly<Record<NameMode, string>>;
110
+ declare const DEFAULT_SCHEMA_SUFFIX = "Schema";
111
+ /** Table name used when a config is checked for invalid or colliding names. */
112
+ declare const AFFIX_PROBE_TABLE = "users";
113
+ /**
114
+ * Real PascalCase, unlike the `cap()` helpers scattered around the repo which only upcase
115
+ * character zero. Splits on `_`, `-`, whitespace and camel boundaries, and leaves the rest of
116
+ * each part alone so acronyms survive (`userID` -> `UserID`, not `Userid`).
117
+ */
118
+ declare function pascalCase(s: string): string;
119
+ declare function applyTableCase(tsName: string, tableCase: TableCase): string;
120
+ /**
121
+ * Fold an `affix` block and the legacy flat `schemaSuffix` into one fully-populated value.
122
+ * `affix.schema.suffix` wins over `schemaSuffix`; `schemaSuffix` wins over the built-in
123
+ * `'Schema'`. Calling with no arguments returns exactly today's naming.
124
+ */
125
+ declare function resolveAffix(opts?: {
126
+ affix?: AffixOptions;
127
+ schemaSuffix?: string;
128
+ }): ResolvedAffix;
129
+ /** Name of the exported schema constant, e.g. `InsertusersSchema`. */
130
+ declare function schemaName(mode: NameMode, tsName: string, affix: ResolvedAffix): string;
131
+ /** Name of the exported type alias, e.g. `InsertusersInput`. */
132
+ declare function typeName(mode: NameMode, tsName: string, affix: ResolvedAffix): string;
133
+ /**
134
+ * Reject affixes that cannot produce a compilable file, before anything is written:
135
+ * - characters that are not legal in a TypeScript identifier
136
+ * - two names in the same declaration space resolving to the same string
137
+ *
138
+ * A schema name equal to a type name is allowed on purpose: `export const X` and
139
+ * `export type X` occupy different declaration spaces, and the generators already emit
140
+ * `type ... = z.input<typeof ...>` pairs.
141
+ *
142
+ * Only what the caller actually wrote in `affix` is checked. The legacy flat `schemaSuffix`
143
+ * is not character-checked, because it never was, and rejecting it now would break configs
144
+ * that parse today.
145
+ */
146
+ declare function validateAffix(affix?: AffixOptions, schemaSuffix?: string): AffixIssue[];
147
+
3
148
  interface Table {
4
149
  name: string;
5
150
  tsName: string;
@@ -17,8 +162,26 @@ interface FormatOptions {
17
162
  interface ValidationGenerateOptions {
18
163
  outDir: string;
19
164
  format?: FormatOptions;
165
+ /**
166
+ * What every generated file is called after the Drizzle export name, e.g. `.zod.ts`
167
+ * yields `users.zod.ts`. The barrel derives its import specifiers from this same value,
168
+ * so a custom suffix keeps resolving.
169
+ */
20
170
  fileSuffix?: string;
171
+ /**
172
+ * How the barrel spells the extension of the files it re-exports. Defaults to `'js'`,
173
+ * so `users.zod.ts` is imported as `./users.zod.js`, the only form that resolves under
174
+ * every `moduleResolution` without a compiler flag. Use `'none'` for the extensionless
175
+ * specifiers drzl emitted before 2.0.
176
+ */
177
+ importExtension?: ImportExtension;
21
178
  schemaSuffix?: string;
179
+ /**
180
+ * Prefixes, suffixes and table casing for the generated identifiers. Omit it and the
181
+ * output is identical to every previous version; `schemaSuffix` stays the fallback for
182
+ * `affix.schema.suffix`.
183
+ */
184
+ affix?: AffixOptions;
22
185
  coerceDates?: 'input' | 'all' | 'none';
23
186
  emit?: {
24
187
  select?: boolean;
@@ -38,4 +201,4 @@ declare function updateColumns(table: Table): Column[];
38
201
  declare function selectColumns(table: Table): Column[];
39
202
  declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
40
203
 
41
- export { type FormatOptions, type Table, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, formatCode, insertColumns, isGeneratedColumn, selectColumns, updateColumns };
204
+ export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, NAME_MODES, type NameMode, type ResolvedAffix, type Table, type TableCase, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, formatCode, importSpecifier, insertColumns, isGeneratedColumn, moduleFileName, moduleSpecifier, pascalCase, resolveAffix, schemaName, selectColumns, typeName, updateColumns, validateAffix };
package/dist/index.js CHANGED
@@ -1,5 +1,136 @@
1
1
  import "./chunk-MKBO26DX.js";
2
2
 
3
+ // src/files.ts
4
+ var IMPORT_EXTENSIONS = ["js", "none", "ts"];
5
+ var DEFAULT_IMPORT_EXTENSION = "js";
6
+ var TS_EXTENSIONS = [
7
+ { ext: ".mts", js: ".mjs", none: ".mjs" },
8
+ { ext: ".cts", js: ".cjs", none: ".cjs" },
9
+ { ext: ".tsx", js: ".js", none: "" },
10
+ { ext: ".ts", js: ".js", none: "" }
11
+ ];
12
+ function moduleFileName(tsName, fileSuffix) {
13
+ return `${tsName}${fileSuffix}`;
14
+ }
15
+ function importSpecifier(relativePath, importExtension = DEFAULT_IMPORT_EXTENSION) {
16
+ for (const { ext, js, none } of TS_EXTENSIONS) {
17
+ if (!relativePath.endsWith(ext)) continue;
18
+ const stem = relativePath.slice(0, -ext.length);
19
+ if (importExtension === "ts") return relativePath;
20
+ return `${stem}${importExtension === "none" ? none : js}`;
21
+ }
22
+ return relativePath;
23
+ }
24
+ function moduleSpecifier(tsName, fileSuffix, importExtension = DEFAULT_IMPORT_EXTENSION) {
25
+ return importSpecifier(`./${moduleFileName(tsName, fileSuffix)}`, importExtension);
26
+ }
27
+
28
+ // src/naming.ts
29
+ var NAME_MODES = ["insert", "update", "select"];
30
+ var DEFAULT_MODE_PREFIX = {
31
+ insert: "Insert",
32
+ update: "Update",
33
+ select: "Select"
34
+ };
35
+ var DEFAULT_TYPE_SUFFIX = {
36
+ insert: "Input",
37
+ update: "Input",
38
+ select: "Output"
39
+ };
40
+ var DEFAULT_SCHEMA_SUFFIX = "Schema";
41
+ var AFFIX_PROBE_TABLE = "users";
42
+ function spread(value, fallback) {
43
+ if (value === void 0) return { ...fallback };
44
+ if (typeof value === "string") return { insert: value, update: value, select: value };
45
+ return {
46
+ insert: value.insert ?? fallback.insert,
47
+ update: value.update ?? fallback.update,
48
+ select: value.select ?? fallback.select
49
+ };
50
+ }
51
+ function pascalCase(s) {
52
+ return s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[\s_-]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
53
+ }
54
+ function applyTableCase(tsName, tableCase) {
55
+ return tableCase === "pascal" ? pascalCase(tsName) : tsName;
56
+ }
57
+ function resolveAffix(opts) {
58
+ const affix = opts?.affix;
59
+ const legacy = opts?.schemaSuffix ?? DEFAULT_SCHEMA_SUFFIX;
60
+ const legacyMap = {
61
+ insert: legacy,
62
+ update: legacy,
63
+ select: legacy
64
+ };
65
+ return {
66
+ tableCase: affix?.tableCase ?? "preserve",
67
+ schema: {
68
+ prefix: spread(affix?.schema?.prefix, DEFAULT_MODE_PREFIX),
69
+ suffix: spread(affix?.schema?.suffix, legacyMap)
70
+ },
71
+ type: {
72
+ prefix: spread(affix?.type?.prefix, DEFAULT_MODE_PREFIX),
73
+ suffix: spread(affix?.type?.suffix, DEFAULT_TYPE_SUFFIX)
74
+ }
75
+ };
76
+ }
77
+ function schemaName(mode, tsName, affix) {
78
+ return affix.schema.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.schema.suffix[mode];
79
+ }
80
+ function typeName(mode, tsName, affix) {
81
+ return affix.type.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.type.suffix[mode];
82
+ }
83
+ var PREFIX_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
84
+ var SUFFIX_RE = /^[A-Za-z0-9_$]+$/;
85
+ function validateAffix(affix, schemaSuffix) {
86
+ const issues = [];
87
+ if (!affix) return issues;
88
+ const checkOne = (value, path, kind) => {
89
+ if (value === "") return;
90
+ const ok = kind === "prefix" ? PREFIX_RE.test(value) : SUFFIX_RE.test(value);
91
+ if (ok) return;
92
+ issues.push({
93
+ path,
94
+ message: `${JSON.stringify(value)} cannot appear in a TypeScript identifier. Use only letters, digits, "_" and "$"` + (kind === "prefix" ? ", and do not start with a digit." : ".")
95
+ });
96
+ };
97
+ const checkValue = (value, base, kind) => {
98
+ if (value === void 0) return;
99
+ if (typeof value === "string") {
100
+ checkOne(value, base, kind);
101
+ return;
102
+ }
103
+ for (const mode of NAME_MODES) {
104
+ const v = value[mode];
105
+ if (v !== void 0) checkOne(v, [...base, mode], kind);
106
+ }
107
+ };
108
+ checkValue(affix.schema?.prefix, ["schema", "prefix"], "prefix");
109
+ checkValue(affix.schema?.suffix, ["schema", "suffix"], "suffix");
110
+ checkValue(affix.type?.prefix, ["type", "prefix"], "prefix");
111
+ checkValue(affix.type?.suffix, ["type", "suffix"], "suffix");
112
+ if (issues.length) return issues;
113
+ const resolved = resolveAffix({ affix, schemaSuffix });
114
+ const collisions = (space, build) => {
115
+ const seen = /* @__PURE__ */ new Map();
116
+ for (const mode of NAME_MODES) {
117
+ const name = build(mode);
118
+ const first = seen.get(name);
119
+ if (first) {
120
+ issues.push({
121
+ path: [space],
122
+ message: `The ${space} names for "${first}" and "${mode}" collide: both resolve to "${name}". All three are emitted into the same file, so at least one prefix or suffix has to differ.`
123
+ });
124
+ } else {
125
+ seen.set(name, mode);
126
+ }
127
+ }
128
+ };
129
+ collisions("schema", (mode) => schemaName(mode, AFFIX_PROBE_TABLE, resolved));
130
+ collisions("type", (mode) => typeName(mode, AFFIX_PROBE_TABLE, resolved));
131
+ return issues;
132
+ }
133
+
3
134
  // src/index.ts
4
135
  function isGeneratedColumn(c, primaryKeyColumns) {
5
136
  return c.isGenerated || primaryKeyColumns.includes(c.name);
@@ -41,9 +172,25 @@ async function formatCode(code, filePath, fmt) {
41
172
  return code;
42
173
  }
43
174
  export {
175
+ AFFIX_PROBE_TABLE,
176
+ DEFAULT_IMPORT_EXTENSION,
177
+ DEFAULT_MODE_PREFIX,
178
+ DEFAULT_SCHEMA_SUFFIX,
179
+ DEFAULT_TYPE_SUFFIX,
180
+ IMPORT_EXTENSIONS,
181
+ NAME_MODES,
182
+ applyTableCase,
44
183
  formatCode,
184
+ importSpecifier,
45
185
  insertColumns,
46
186
  isGeneratedColumn,
187
+ moduleFileName,
188
+ moduleSpecifier,
189
+ pascalCase,
190
+ resolveAffix,
191
+ schemaName,
47
192
  selectColumns,
48
- updateColumns
193
+ typeName,
194
+ updateColumns,
195
+ validateAffix
49
196
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/validation-core",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "sideEffects": false,
13
13
  "dependencies": {
14
- "@drzl/analyzer": "^1.2.0"
14
+ "@drzl/analyzer": "^1.3.0"
15
15
  },
16
16
  "devDependencies": {
17
17
  "tsup": "^8.5.0",