cogsbox-shape 0.5.65 → 0.5.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/schema.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { z } from "zod";
2
- import zodToJsonSchema from "zod-to-json-schema";
3
2
  export const isFunction = (fn) => typeof fn === "function";
4
3
  // Function to create a properly typed current timestamp config
5
4
  export function currentTimeStamp() {
@@ -9,36 +8,36 @@ export function currentTimeStamp() {
9
8
  };
10
9
  }
11
10
  // Now define the shape object with the explicit type annotation
12
- export const shape = {
13
- int: (config = {}) => shape.sql({
11
+ export const s = {
12
+ int: (config = {}) => s.sql({
14
13
  type: "int",
15
14
  ...config,
16
15
  }),
17
- varchar: (config = {}) => shape.sql({
16
+ varchar: (config = {}) => s.sql({
18
17
  type: "varchar",
19
18
  ...config,
20
19
  }),
21
- char: (config = {}) => shape.sql({
20
+ char: (config = {}) => s.sql({
22
21
  type: "char",
23
22
  ...config,
24
23
  }),
25
- text: (config = {}) => shape.sql({
24
+ text: (config = {}) => s.sql({
26
25
  type: "text",
27
26
  ...config,
28
27
  }),
29
- longtext: (config = {}) => shape.sql({
28
+ longtext: (config = {}) => s.sql({
30
29
  type: "longtext",
31
30
  ...config,
32
31
  }),
33
- boolean: (config = {}) => shape.sql({
32
+ boolean: (config = {}) => s.sql({
34
33
  type: "boolean",
35
34
  ...config,
36
35
  }),
37
- date: (config = {}) => shape.sql({
36
+ date: (config = {}) => s.sql({
38
37
  type: "date",
39
38
  ...config,
40
39
  }),
41
- datetime: (config = {}) => shape.sql({
40
+ datetime: (config = {}) => s.sql({
42
41
  type: "datetime",
43
42
  ...config,
44
43
  }),
@@ -144,41 +143,36 @@ function createBuilder(config) {
144
143
  clientTransform: config.clientTransform, // <-- FIX: Make sure transform is passed through
145
144
  validationTransform: config.validationTransform, // <-- FIX: Make sure transform is passed through
146
145
  },
147
- initialState: (
148
- // ... this initialState function remains unchanged ...
149
- schemaOrDefault, defaultValue) => {
146
+ initialState: (schemaOrDefault, defaultValue) => {
150
147
  if (completedStages.has("new")) {
151
148
  throw new Error("initialState() can only be called once in the chain");
152
149
  }
153
- if (completedStages.has("client")) {
154
- throw new Error("initialState() must be called before client()");
155
- }
156
- if (completedStages.has("validation")) {
157
- throw new Error("initialState() must be called before validation()");
158
- }
159
- const hasTypeParam = defaultValue !== undefined;
160
- const newSchema = hasTypeParam
150
+ // ... other error checks ...
151
+ const hasSchemaArg = defaultValue !== undefined;
152
+ // This logic is mostly from your original code.
153
+ const newSchema = hasSchemaArg
161
154
  ? isFunction(schemaOrDefault)
162
155
  ? schemaOrDefault({ sql: config.sqlZod })
163
156
  : schemaOrDefault
164
- : config.sqlZod;
165
- const finalDefaultValue = hasTypeParam
166
- ? defaultValue
157
+ : config.sqlZod; // If only a primitive is passed, the "new" schema is still the SQL one.
158
+ const finalDefaultValue = hasSchemaArg
159
+ ? defaultValue()
167
160
  : schemaOrDefault;
168
161
  const newCompletedStages = new Set(completedStages);
169
162
  newCompletedStages.add("new");
170
- const newClientZod = hasTypeParam
163
+ // ---- THIS IS THE RUNTIME FIX THAT MATCHES YOUR INTERFACE ----
164
+ // If a new schema was passed, create a union.
165
+ // If ONLY a primitive was passed, we MUST also create a union.
166
+ const newClientZod = hasSchemaArg
171
167
  ? z.union([config.sqlZod, newSchema])
172
- : config.sqlZod;
168
+ : z.union([config.sqlZod, z.any()]); // Create the union for the primitive case
173
169
  return createBuilder({
174
170
  ...config,
175
171
  stage: "new",
176
172
  newZod: newSchema,
177
173
  initialValue: finalDefaultValue,
178
174
  clientZod: newClientZod,
179
- validationZod: hasTypeParam
180
- ? z.union([config.sqlZod, newSchema])
181
- : config.sqlZod,
175
+ validationZod: newClientZod, // Keep validation and client in sync for this step
182
176
  completedStages: newCompletedStages,
183
177
  });
184
178
  },
@@ -191,7 +185,6 @@ function createBuilder(config) {
191
185
  }
192
186
  const newCompletedStages = new Set(completedStages);
193
187
  newCompletedStages.add("client");
194
- // ---- THIS IS THE MAIN FIX ----
195
188
  if (config.stage === "relation") {
196
189
  return createBuilder({
197
190
  ...config,
@@ -263,55 +256,46 @@ function createBuilder(config) {
263
256
  };
264
257
  return builderObject;
265
258
  }
266
- export function hasMany(config) {
267
- return () => ({
268
- type: "hasMany",
269
- fromKey: config.fromKey,
270
- toKey: config.toKey(),
271
- schema: config.schema(),
272
- defaultCount: config.defaultCount,
273
- });
274
- }
275
- export function hasOne(config) {
276
- return () => ({
277
- type: "hasOne",
278
- fromKey: config.fromKey,
279
- toKey: config.toKey(),
280
- schema: config.schema(),
281
- });
282
- }
283
- export function belongsTo(config) {
284
- return () => ({
285
- type: "belongsTo",
286
- fromKey: config.fromKey,
287
- toKey: config.toKey(),
288
- schema: config.schema(),
289
- });
290
- }
291
- export function manyToMany(config) {
292
- return () => ({
293
- type: "manyToMany",
294
- fromKey: config.fromKey,
295
- toKey: config.toKey(),
296
- schema: config.schema(),
297
- defaultCount: config.defaultCount,
298
- });
259
+ // The table function that enriches fields with their key information
260
+ const SchemaWrapperBrand = Symbol("SchemaWrapper");
261
+ // Update the schema function to use the symbol
262
+ export function schema(schema) {
263
+ const enrichedSchema = {
264
+ _tableName: schema._tableName,
265
+ [SchemaWrapperBrand]: true, // Add the symbol property
266
+ };
267
+ for (const key in schema) {
268
+ if (key !== "_tableName" &&
269
+ Object.prototype.hasOwnProperty.call(schema, key)) {
270
+ enrichedSchema[key] = {
271
+ ...schema[key],
272
+ __meta: {
273
+ _key: key,
274
+ _fieldType: schema[key],
275
+ },
276
+ __parentTableType: schema,
277
+ };
278
+ }
279
+ }
280
+ return enrichedSchema;
299
281
  }
300
282
  function inferDefaultFromZod(zodType, sqlConfig) {
301
- // Handle relation configs
302
283
  if (sqlConfig && typeof sqlConfig === "object" && "type" in sqlConfig) {
303
- // Check if it's a relation config by looking for relation types
284
+ // --- THIS IS THE NEW, HIGHEST-PRIORITY CHECK ---
285
+ // If a `default` property exists directly on the SQL config, use it.
286
+ if ("default" in sqlConfig && sqlConfig.default !== undefined) {
287
+ // Exclude CURRENT_TIMESTAMP as it's a special keyword, not a value.
288
+ if (sqlConfig.default === "CURRENT_TIMESTAMP") {
289
+ return new Date();
290
+ }
291
+ return sqlConfig.default;
292
+ }
293
+ // Check if it's a relation config (this logic is fine)
304
294
  if (typeof sqlConfig.type === "string" &&
305
295
  ["hasMany", "hasOne", "belongsTo", "manyToMany"].includes(sqlConfig.type)) {
306
- const relationConfig = sqlConfig;
307
- if (relationConfig.type === "hasMany" ||
308
- relationConfig.type === "manyToMany") {
309
- return Array.from({ length: relationConfig.defaultCount || 0 }, () => ({}));
310
- }
311
- // For hasOne and belongsTo
312
- return {};
296
+ // ... your existing relation logic is fine ...
313
297
  }
314
- // Handle SQL configs (existing logic)
298
+ // Handle SQL type-based generation (this is the fallback)
315
299
  const sqlTypeConfig = sqlConfig;
316
300
  if (sqlTypeConfig.type && !sqlTypeConfig.nullable) {
317
301
  switch (sqlTypeConfig.type) {
@@ -321,7 +305,7 @@ function inferDefaultFromZod(zodType, sqlConfig) {
321
305
  case "longtext":
322
306
  return "";
323
307
  case "int":
324
- return 0;
308
+ return 0; // This is now only used if no `default` is provided
325
309
  case "boolean":
326
310
  return false;
327
311
  case "date":
@@ -333,11 +317,10 @@ function inferDefaultFromZod(zodType, sqlConfig) {
333
317
  return null;
334
318
  }
335
319
  }
336
- // Fall back to existing zod-based inference
320
+ // Fall back to Zod-based inference (this logic is fine)
337
321
  if (zodType instanceof z.ZodOptional) {
338
322
  return undefined;
339
323
  }
340
- // Check for explicit default last
341
324
  if (zodType instanceof z.ZodDefault && zodType._def?.defaultValue) {
342
325
  return typeof zodType._def.defaultValue === "function"
343
326
  ? zodType._def.defaultValue()
@@ -345,12 +328,12 @@ function inferDefaultFromZod(zodType, sqlConfig) {
345
328
  }
346
329
  return undefined;
347
330
  }
348
- export function reference(config) {
349
- return {
350
- type: "reference",
351
- to: config,
352
- };
353
- }
331
+ // export function reference<TField extends object>(config: TField) {
332
+ // return {
333
+ // type: "reference" as const,
334
+ // to: config,
335
+ // };
336
+ // }
354
337
  export function createMixedValidationSchema(schema, clientSchema, dbSchema) {
355
338
  // If schemas are provided, use them (to avoid circular calls)
356
339
  if (clientSchema && dbSchema) {
@@ -422,109 +405,197 @@ function isRelation(value) {
422
405
  "toKey" in value &&
423
406
  "schema" in value);
424
407
  }
425
- export function createSchema(schema) {
408
+ export function createSchema(schema, relations) {
426
409
  const sqlFields = {};
427
410
  const clientFields = {};
428
411
  const validationFields = {};
429
412
  const defaultValues = {};
430
- const deferredFields = [];
431
- // --- PASS 1: Separate immediate fields from deferred relations/references ---
413
+ const fieldTransforms = {};
414
+ // --- PASS 1: Process main schema fields (no relations here) ---
432
415
  for (const key in schema) {
433
416
  if (key === "_tableName" || key.startsWith("__"))
434
417
  continue;
435
418
  const definition = schema[key];
436
- if ((definition && typeof definition.config === "object") || // It's a builder
437
- typeof definition === "function" || // It's a legacy relation
438
- (definition && definition.type === "reference") // It's a reference
439
- ) {
440
- // Defer all builders, functions, and references to Pass 2
441
- deferredFields.push({ key, definition });
442
- }
443
- else {
444
- // This case should ideally not be hit with the builder pattern, but is safe to have.
445
- // Process any non-builder, non-deferred fields if they exist.
419
+ if (definition && definition.type === "reference") {
420
+ // Handle reference fields
421
+ const referencedFieldBuilder = definition.to();
422
+ const referencedConfig = referencedFieldBuilder.config;
423
+ sqlFields[key] = referencedConfig.zodSqlSchema;
424
+ clientFields[key] = referencedConfig.zodClientSchema;
425
+ validationFields[key] = referencedConfig.zodValidationSchema;
426
+ // Foreign key fields should get their own default, not the referenced field's default
427
+ defaultValues[key] = inferDefaultFromZod(referencedConfig.zodClientSchema, { ...referencedConfig.sql, default: undefined });
446
428
  }
447
- }
448
- // --- PASS 2: Process all deferred references and relations ---
449
- for (const { key, definition } of deferredFields) {
450
- let resolvedDefinition = definition;
451
- if (typeof resolvedDefinition === "function") {
452
- // Handle legacy function style: hasMany(...)
453
- resolvedDefinition = resolvedDefinition();
454
- const relation = resolvedDefinition;
455
- const childSchemaResult = createSchema(relation.schema);
456
- if (relation.type === "hasMany" || relation.type === "manyToMany") {
457
- sqlFields[key] = z.array(childSchemaResult.sqlSchema).optional();
458
- clientFields[key] = z.array(childSchemaResult.clientSchema).optional();
459
- validationFields[key] = z
460
- .array(childSchemaResult.validationSchema)
461
- .optional();
462
- defaultValues[key] = Array.from({ length: relation.defaultCount || 0 }, () => childSchemaResult.defaultValues);
429
+ else if (definition && definition.config) {
430
+ // Handle regular fields with builder pattern
431
+ const config = definition.config;
432
+ sqlFields[key] = config.zodSqlSchema;
433
+ clientFields[key] = config.zodClientSchema;
434
+ validationFields[key] = config.zodValidationSchema;
435
+ if (config.transforms) {
436
+ fieldTransforms[key] = config.transforms;
437
+ }
438
+ // Handle initial value
439
+ const initialValueOrFn = config.initialValue;
440
+ if (isFunction(initialValueOrFn)) {
441
+ defaultValues[key] = initialValueOrFn();
463
442
  }
464
443
  else {
465
- sqlFields[key] = childSchemaResult.sqlSchema.optional();
466
- clientFields[key] = childSchemaResult.clientSchema.optional();
467
- validationFields[key] = childSchemaResult.validationSchema.optional();
468
- defaultValues[key] = childSchemaResult.defaultValues;
444
+ defaultValues[key] = initialValueOrFn;
469
445
  }
470
446
  }
471
- else if (resolvedDefinition && resolvedDefinition.type === "reference") {
472
- // Handle reference fields
473
- const referencedField = resolvedDefinition.to();
474
- sqlFields[key] = referencedField.config.zodSqlSchema;
475
- clientFields[key] = referencedField.config.zodClientSchema;
476
- validationFields[key] = referencedField.config.zodValidationSchema;
477
- defaultValues[key] = referencedField.config.initialValue;
478
- }
479
- else if (resolvedDefinition && resolvedDefinition.config) {
480
- // It's a builder object (`shape.sql(...)` or `shape.hasMany(...)`)
481
- const config = resolvedDefinition.config;
482
- const sqlConfig = config.sql;
483
- if (sqlConfig &&
484
- typeof sqlConfig === "object" &&
485
- ["hasMany", "hasOne", "belongsTo", "manyToMany"].includes(sqlConfig.type)) {
486
- // --- THIS IS THE KEY PART FOR RELATION BUILDERS ---
487
- const relationConfig = sqlConfig;
488
- const childSchemaResult = createSchema(relationConfig.schema);
489
- // 1. Create the BASE schema WITHOUT .optional()
490
- let rawClientSchema;
491
- let rawValidationSchema;
492
- if (relationConfig.type === "hasMany" ||
493
- relationConfig.type === "manyToMany") {
494
- rawClientSchema = z.array(childSchemaResult.clientSchema);
495
- rawValidationSchema = z.array(childSchemaResult.validationSchema);
496
- defaultValues[key] = Array.from({ length: relationConfig.defaultCount || 0 }, () => childSchemaResult.defaultValues);
497
- }
498
- else {
499
- rawClientSchema = childSchemaResult.clientSchema;
500
- rawValidationSchema = childSchemaResult.validationSchema;
501
- defaultValues[key] = childSchemaResult.defaultValues;
447
+ }
448
+ // --- PASS 2: Process relations if provided ---
449
+ if (relations) {
450
+ for (const key in relations) {
451
+ const relationDefinition = relations[key];
452
+ if (relationDefinition && relationDefinition.config) {
453
+ const config = relationDefinition.config;
454
+ const sqlConfig = config.sql;
455
+ if (sqlConfig &&
456
+ typeof sqlConfig === "object" &&
457
+ ["hasMany", "hasOne", "belongsTo", "manyToMany"].includes(sqlConfig.type)) {
458
+ const relationConfig = sqlConfig;
459
+ const childSchemaResult = createSchema(relationConfig.schema());
460
+ // Create the base schemas based on relation type
461
+ let baseSqlSchema;
462
+ let baseClientSchema;
463
+ let baseValidationSchema;
464
+ if (relationConfig.type === "hasMany" ||
465
+ relationConfig.type === "manyToMany") {
466
+ baseSqlSchema = z.array(childSchemaResult.sqlSchema);
467
+ baseClientSchema = z.array(childSchemaResult.clientSchema);
468
+ baseValidationSchema = z.array(childSchemaResult.validationSchema);
469
+ defaultValues[key] = Array.from({ length: relationConfig.defaultCount || 0 }, () => childSchemaResult.defaultValues);
470
+ }
471
+ else {
472
+ baseSqlSchema = childSchemaResult.sqlSchema;
473
+ baseClientSchema = childSchemaResult.clientSchema;
474
+ baseValidationSchema = childSchemaResult.validationSchema;
475
+ defaultValues[key] = childSchemaResult.defaultValues;
476
+ }
477
+ // Apply transforms if they exist
478
+ const finalClientSchema = config.clientTransform
479
+ ? config.clientTransform(baseClientSchema)
480
+ : baseClientSchema;
481
+ const finalValidationSchema = config.validationTransform
482
+ ? config.validationTransform(baseValidationSchema)
483
+ : finalClientSchema;
484
+ // Assign the schemas
485
+ sqlFields[key] = baseSqlSchema.optional(); // SQL fields are optional for lazy loading
486
+ clientFields[key] = finalClientSchema;
487
+ validationFields[key] = finalValidationSchema;
502
488
  }
503
- // 2. Apply the transform to the RAW schema
504
- const transformedClientSchema = config.clientTransform
505
- ? config.clientTransform(rawClientSchema)
506
- : rawClientSchema;
507
- const transformedValidationSchema = config.validationTransform
508
- ? config.validationTransform(rawValidationSchema)
509
- : transformedClientSchema;
510
- // 3. Assign the final schemas. NO .optional() is added.
511
- sqlFields[key] = z.array(childSchemaResult.sqlSchema).optional(); // SQL is still optional, as it might not be loaded.
512
- clientFields[key] = transformedClientSchema; // <-- NO .optional()
513
- validationFields[key] = transformedValidationSchema; // <-- NO .optional()
514
- }
515
- else {
516
- // It's a standard field builder (`shape.sql(...)`)
517
- sqlFields[key] = config.zodSqlSchema;
518
- clientFields[key] = config.zodClientSchema;
519
- validationFields[key] = config.zodValidationSchema;
520
- defaultValues[key] = config.initialValue;
521
489
  }
522
490
  }
523
491
  }
492
+ // Create transform functions
493
+ const toClient = (dbObject) => {
494
+ const clientObject = { ...dbObject };
495
+ for (const key in fieldTransforms) {
496
+ if (key in clientObject && clientObject[key] !== undefined) {
497
+ clientObject[key] = fieldTransforms[key].toClient(clientObject[key]);
498
+ }
499
+ }
500
+ return clientObject;
501
+ };
502
+ const toDb = (clientObject) => {
503
+ const dbObject = { ...clientObject };
504
+ for (const key in fieldTransforms) {
505
+ if (key in dbObject && dbObject[key] !== undefined) {
506
+ dbObject[key] = fieldTransforms[key].toDb(dbObject[key]);
507
+ }
508
+ }
509
+ return dbObject;
510
+ };
524
511
  return {
525
512
  sqlSchema: z.object(sqlFields),
526
513
  clientSchema: z.object(clientFields),
527
514
  validationSchema: z.object(validationFields),
528
515
  defaultValues: defaultValues,
516
+ toClient,
517
+ toDb,
518
+ };
519
+ }
520
+ export function schemaRelations(baseSchema, referencesBuilder) {
521
+ const rel = {
522
+ reference: (fieldGetter) => ({
523
+ type: "reference",
524
+ to: fieldGetter,
525
+ }),
526
+ hasMany: (config) => {
527
+ const relationConfig = {
528
+ type: "hasMany",
529
+ fromKey: config.fromKey,
530
+ toKey: () => config.toKey.__meta._key,
531
+ schema: () => config.toKey.__parentTableType,
532
+ defaultCount: config.defaultCount,
533
+ };
534
+ const placeholderSchema = z.array(z.any());
535
+ return createBuilder({
536
+ stage: "relation",
537
+ sqlConfig: relationConfig,
538
+ sqlZod: placeholderSchema,
539
+ newZod: placeholderSchema,
540
+ initialValue: Array.from({ length: config.defaultCount || 0 }, () => ({})),
541
+ clientZod: placeholderSchema,
542
+ validationZod: placeholderSchema,
543
+ }); // FIX: This is a hack to get around the circular reference
544
+ },
545
+ hasOne: (config) => {
546
+ const relationConfig = {
547
+ type: "hasOne",
548
+ fromKey: config.fromKey,
549
+ toKey: config.toKey,
550
+ schema: config.schema,
551
+ };
552
+ const relationZodType = z.any();
553
+ return createBuilder({
554
+ stage: "relation",
555
+ sqlConfig: relationConfig,
556
+ sqlZod: relationZodType,
557
+ newZod: relationZodType,
558
+ initialValue: {},
559
+ clientZod: relationZodType,
560
+ validationZod: relationZodType,
561
+ });
562
+ },
563
+ manyToMany: (config) => {
564
+ const relationConfig = {
565
+ type: "manyToMany",
566
+ fromKey: config.fromKey,
567
+ toKey: config.toKey,
568
+ schema: config.schema,
569
+ ...(config.defaultCount !== undefined && {
570
+ defaultCount: config.defaultCount,
571
+ }),
572
+ };
573
+ const relationZodType = z.array(z.any()).optional();
574
+ return createBuilder({
575
+ stage: "relation",
576
+ sqlConfig: relationConfig,
577
+ sqlZod: relationZodType,
578
+ newZod: relationZodType,
579
+ initialValue: Array.from({ length: config.defaultCount || 0 }, () => ({})),
580
+ clientZod: relationZodType,
581
+ validationZod: relationZodType,
582
+ });
583
+ },
529
584
  };
585
+ const refs = referencesBuilder(rel);
586
+ const enrichedRefs = {};
587
+ // Enrich each field in the refs object with __meta and __parentTableType
588
+ for (const key in refs) {
589
+ if (Object.prototype.hasOwnProperty.call(refs, key)) {
590
+ enrichedRefs[key] = {
591
+ ...refs[key],
592
+ __meta: {
593
+ _key: key,
594
+ _fieldType: refs[key],
595
+ },
596
+ __parentTableType: baseSchema,
597
+ };
598
+ }
599
+ }
600
+ return enrichedRefs;
530
601
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-shape",
3
- "version": "0.5.65",
3
+ "version": "0.5.67",
4
4
  "description": "A TypeScript library for creating type-safe database schemas with Zod validation, SQL type definitions, and automatic client/server transformations. Unifies client, server, and database types through a single schema definition, with built-in support for relationships and serialization.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,7 +11,8 @@
11
11
  "scripts": {
12
12
  "build": " tsc",
13
13
  "lint": "eslint src --ext .ts",
14
- "format": "prettier --write \"src/**/*.ts\""
14
+ "format": "prettier --write \"src/**/*.ts\"",
15
+ "test": "vitest "
15
16
  },
16
17
  "bin": {
17
18
  "cogsbox-shape": "tsx ./dist/cli.js"
@@ -44,8 +45,11 @@
44
45
  "@typescript-eslint/eslint-plugin": "^6.15.0",
45
46
  "@typescript-eslint/parser": "^6.15.0",
46
47
  "eslint": "^8.56.0",
48
+ "expect-type": "^1.2.1",
47
49
  "prettier": "^3.1.1",
48
- "typescript": "^5.3.3"
50
+ "typescript": "^5.3.3",
51
+ "vitest": "^3.2.0",
52
+ "zod": "^3.25.67"
49
53
  },
50
54
  "peerDependencies": {
51
55
  "zod": "^3.22.4"