@farming-labs/orm 0.0.8 → 0.0.10

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/index.cjs CHANGED
@@ -32,12 +32,17 @@ __export(index_exports, {
32
32
  hasOne: () => hasOne,
33
33
  id: () => id,
34
34
  manyToMany: () => manyToMany,
35
+ mergeUniqueLookupCreateData: () => mergeUniqueLookupCreateData,
35
36
  model: () => model,
36
37
  renderDrizzleSchema: () => renderDrizzleSchema,
37
38
  renderPrismaSchema: () => renderPrismaSchema,
38
39
  renderSafeSql: () => renderSafeSql,
39
40
  replaceGeneratedBlock: () => replaceGeneratedBlock,
40
- string: () => string
41
+ requireUniqueLookup: () => requireUniqueLookup,
42
+ resolveRowIdentityLookup: () => resolveRowIdentityLookup,
43
+ string: () => string,
44
+ toUniqueLookupWhere: () => toUniqueLookupWhere,
45
+ validateUniqueLookupUpdateData: () => validateUniqueLookupUpdateData
41
46
  });
42
47
  module.exports = __toCommonJS(index_exports);
43
48
 
@@ -187,6 +192,199 @@ function datetime() {
187
192
  }
188
193
 
189
194
  // src/manifest.ts
195
+ function equalLookupValues(left, right) {
196
+ if (left instanceof Date && right instanceof Date) {
197
+ return left.getTime() === right.getTime();
198
+ }
199
+ return Object.is(left, right);
200
+ }
201
+ function isFilterObject(value) {
202
+ return !!value && typeof value === "object" && !(value instanceof Date) && !Array.isArray(value);
203
+ }
204
+ function extractEqualityValue(filter) {
205
+ if (!isFilterObject(filter)) {
206
+ return {
207
+ supported: true,
208
+ value: filter
209
+ };
210
+ }
211
+ const keys = Object.keys(filter);
212
+ if (keys.length === 1 && "eq" in filter) {
213
+ return {
214
+ supported: true,
215
+ value: filter.eq
216
+ };
217
+ }
218
+ return {
219
+ supported: false,
220
+ value: void 0
221
+ };
222
+ }
223
+ function requireEqualityValues(model2, where, operation) {
224
+ const keys = Object.keys(where).filter((key) => key !== "AND" && key !== "OR" && key !== "NOT");
225
+ if ("AND" in where || "OR" in where || "NOT" in where || keys.length === 0) {
226
+ throw new Error(
227
+ `${operation} on model "${model2.name}" requires a unique equality filter in "where".`
228
+ );
229
+ }
230
+ const values = {};
231
+ for (const fieldName of keys) {
232
+ const field = model2.fields[fieldName];
233
+ if (!field) {
234
+ throw new Error(`Unknown field "${fieldName}" on model "${model2.name}".`);
235
+ }
236
+ const { supported, value } = extractEqualityValue(where[fieldName]);
237
+ if (!supported || value === void 0 || value === null) {
238
+ throw new Error(
239
+ `${operation} on model "${model2.name}" requires the "where" field "${fieldName}" to use a single non-null equality value.`
240
+ );
241
+ }
242
+ values[fieldName] = value;
243
+ }
244
+ return values;
245
+ }
246
+ function sameFieldSet(left, right) {
247
+ return left.length === right.length && left.every((fieldName) => right.includes(fieldName));
248
+ }
249
+ function requireUniqueLookup(model2, where, operation) {
250
+ const values = requireEqualityValues(model2, where, operation);
251
+ const keys = Object.keys(values);
252
+ if (keys.length === 1) {
253
+ const field = model2.fields[keys[0]];
254
+ if (field.kind === "id") {
255
+ return {
256
+ kind: "id",
257
+ fields: [field],
258
+ values
259
+ };
260
+ }
261
+ if (field.unique) {
262
+ return {
263
+ kind: "field",
264
+ fields: [field],
265
+ values
266
+ };
267
+ }
268
+ }
269
+ const constraint = model2.constraints.unique.find(
270
+ (candidate) => sameFieldSet([...candidate.fields], keys)
271
+ );
272
+ if (!constraint) {
273
+ throw new Error(
274
+ `${operation} on model "${model2.name}" requires the "where" clause to match an id field, unique field, or declared unique constraint using equality values only.`
275
+ );
276
+ }
277
+ return {
278
+ kind: "constraint",
279
+ fields: constraint.fields.map((fieldName) => model2.fields[fieldName]),
280
+ values: Object.fromEntries(
281
+ constraint.fields.map((fieldName) => [fieldName, values[fieldName]])
282
+ ),
283
+ constraint
284
+ };
285
+ }
286
+ function resolveRowIdentityLookup(model2, row) {
287
+ const idField = model2.fields.id;
288
+ if (idField && row[idField.name] !== void 0 && row[idField.name] !== null) {
289
+ return {
290
+ kind: "id",
291
+ fields: [idField],
292
+ values: {
293
+ [idField.name]: row[idField.name]
294
+ }
295
+ };
296
+ }
297
+ const uniqueField = Object.values(model2.fields).find(
298
+ (field) => field.unique && row[field.name] !== void 0 && row[field.name] !== null
299
+ );
300
+ if (uniqueField) {
301
+ return {
302
+ kind: "field",
303
+ fields: [uniqueField],
304
+ values: {
305
+ [uniqueField.name]: row[uniqueField.name]
306
+ }
307
+ };
308
+ }
309
+ for (const constraint of model2.constraints.unique) {
310
+ if (constraint.fields.every(
311
+ (fieldName) => row[fieldName] !== void 0 && row[fieldName] !== null
312
+ )) {
313
+ return {
314
+ kind: "constraint",
315
+ fields: constraint.fields.map((fieldName) => model2.fields[fieldName]),
316
+ values: Object.fromEntries(
317
+ constraint.fields.map((fieldName) => [fieldName, row[fieldName]])
318
+ ),
319
+ constraint
320
+ };
321
+ }
322
+ }
323
+ throw new Error(
324
+ `Model "${model2.name}" requires an "id" field, unique field, or declared unique constraint with non-null values for identity lookups.`
325
+ );
326
+ }
327
+ function toUniqueLookupWhere(lookup) {
328
+ return Object.fromEntries(lookup.fields.map((field) => [field.name, lookup.values[field.name]]));
329
+ }
330
+ function mergeUniqueLookupCreateData(model2, createData, lookup, operation) {
331
+ const output = {
332
+ ...createData
333
+ };
334
+ for (const field of lookup.fields) {
335
+ const currentValue = output[field.name];
336
+ const expectedValue = lookup.values[field.name];
337
+ if (currentValue !== void 0 && !equalLookupValues(currentValue, expectedValue)) {
338
+ throw new Error(
339
+ `${operation} on model "${model2.name}" requires create.${field.name} to match where.${field.name}.`
340
+ );
341
+ }
342
+ output[field.name] = currentValue ?? expectedValue;
343
+ }
344
+ return output;
345
+ }
346
+ function validateUniqueLookupUpdateData(model2, updateData, lookup, operation) {
347
+ for (const field of lookup.fields) {
348
+ const nextValue = updateData[field.name];
349
+ if (nextValue !== void 0 && !equalLookupValues(nextValue, lookup.values[field.name])) {
350
+ throw new Error(
351
+ `${operation} on model "${model2.name}" cannot change the conflict field "${field.name}".`
352
+ );
353
+ }
354
+ }
355
+ }
356
+ function createConstraintName(table, columns, suffix) {
357
+ const base = [table, ...columns].join("_").replace(/[^a-zA-Z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
358
+ return `${base}_${suffix}`;
359
+ }
360
+ function normalizeConstraints(modelName, table, fields, constraints) {
361
+ const normalize = (entries, unique) => (entries ?? []).map((entry) => {
362
+ if (!entry.length) {
363
+ throw new Error(
364
+ `Model "${modelName}" defines an empty ${unique ? "unique" : "index"} constraint.`
365
+ );
366
+ }
367
+ const columns = entry.map((fieldName) => {
368
+ const field = fields[fieldName];
369
+ if (!field) {
370
+ throw new Error(
371
+ `Model "${modelName}" defines a ${unique ? "unique" : "index"} constraint on unknown field "${fieldName}".`
372
+ );
373
+ }
374
+ return field.column;
375
+ });
376
+ return {
377
+ name: createConstraintName(table, columns, unique ? "unique" : "idx"),
378
+ fields: [...entry],
379
+ columns,
380
+ unique
381
+ };
382
+ });
383
+ return {
384
+ unique: normalize(constraints.unique, true),
385
+ indexes: normalize(constraints.indexes, false)
386
+ };
387
+ }
190
388
  function createManifest(schema) {
191
389
  const models = Object.fromEntries(
192
390
  Object.entries(schema.models).map(
@@ -214,7 +412,13 @@ function createManifest(schema) {
214
412
  table: definition.table,
215
413
  description: definition.description,
216
414
  fields,
217
- relations: definition.relations
415
+ relations: definition.relations,
416
+ constraints: normalizeConstraints(
417
+ name,
418
+ definition.table,
419
+ fields,
420
+ definition.constraints
421
+ )
218
422
  }
219
423
  ];
220
424
  }
@@ -226,6 +430,7 @@ function createManifest(schema) {
226
430
  // src/generators.ts
227
431
  var capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1);
228
432
  var pluralize = (value) => value.endsWith("s") ? value : `${value}s`;
433
+ var camelize = (value) => value.replace(/[^a-zA-Z0-9]+(.)/g, (_, char) => char.toUpperCase()).replace(/^[^a-zA-Z]+/, "").replace(/^[A-Z]/, (char) => char.toLowerCase());
229
434
  function resolveReferenceTarget(manifest, model2, foreignKey, fallbackTarget) {
230
435
  const reference = model2.fields[foreignKey]?.references;
231
436
  if (!reference) {
@@ -260,6 +465,16 @@ function prismaType(field) {
260
465
  return "DateTime";
261
466
  }
262
467
  }
468
+ function drizzleConstraintProperty(constraint) {
469
+ return camelize(constraint.name) || "constraint";
470
+ }
471
+ function constrainedFields(model2) {
472
+ return new Set(
473
+ [...model2.constraints.unique, ...model2.constraints.indexes].flatMap(
474
+ (constraint) => constraint.fields
475
+ )
476
+ );
477
+ }
263
478
  function drizzleImports(dialect, manifest) {
264
479
  const models = Object.values(manifest.models);
265
480
  const needsBoolean = models.some(
@@ -268,12 +483,17 @@ function drizzleImports(dialect, manifest) {
268
483
  const needsDate = models.some(
269
484
  (model2) => Object.values(model2.fields).some((field) => field.kind === "datetime")
270
485
  );
486
+ const needsIndexes = models.some(
487
+ (model2) => model2.constraints.indexes.length || model2.constraints.unique.length
488
+ );
271
489
  if (dialect === "pg") {
272
490
  return [
273
491
  "pgTable",
274
492
  "text",
275
493
  needsBoolean ? "boolean" : null,
276
- needsDate ? "timestamp" : null
494
+ needsDate ? "timestamp" : null,
495
+ needsIndexes ? "index" : null,
496
+ needsIndexes ? "uniqueIndex" : null
277
497
  ].filter(Boolean);
278
498
  }
279
499
  if (dialect === "mysql") {
@@ -282,12 +502,20 @@ function drizzleImports(dialect, manifest) {
282
502
  "varchar",
283
503
  "text",
284
504
  needsBoolean ? "boolean" : null,
285
- needsDate ? "datetime" : null
505
+ needsDate ? "datetime" : null,
506
+ needsIndexes ? "index" : null,
507
+ needsIndexes ? "uniqueIndex" : null
286
508
  ].filter(Boolean);
287
509
  }
288
- return ["sqliteTable", "text", "integer"];
510
+ return [
511
+ "sqliteTable",
512
+ "text",
513
+ "integer",
514
+ needsIndexes ? "index" : null,
515
+ needsIndexes ? "uniqueIndex" : null
516
+ ].filter(Boolean);
289
517
  }
290
- function drizzleColumn(field, dialect) {
518
+ function drizzleColumn(field, dialect, options = {}) {
291
519
  if (field.kind === "id") {
292
520
  if (dialect === "mysql") {
293
521
  return `varchar("${field.column}", { length: 191 }).primaryKey()`;
@@ -296,7 +524,7 @@ function drizzleColumn(field, dialect) {
296
524
  }
297
525
  if (field.kind === "string") {
298
526
  if (dialect === "mysql") {
299
- const base = field.unique || field.references ? `varchar("${field.column}", { length: 191 })` : `text("${field.column}")`;
527
+ const base = field.unique || field.references || options.indexed ? `varchar("${field.column}", { length: 191 })` : `text("${field.column}")`;
300
528
  return `${base}${field.nullable ? "" : ".notNull()"}${field.unique ? ".unique()" : ""}${field.defaultValue !== void 0 ? `.default(${JSON.stringify(field.defaultValue)})` : ""}`;
301
529
  }
302
530
  return `text("${field.column}")${field.nullable ? "" : ".notNull()"}${field.unique ? ".unique()" : ""}${field.defaultValue !== void 0 ? `.default(${JSON.stringify(field.defaultValue)})` : ""}`;
@@ -313,14 +541,14 @@ function drizzleColumn(field, dialect) {
313
541
  if (dialect === "sqlite") {
314
542
  return `integer("${field.column}", { mode: "timestamp" })${field.nullable ? "" : ".notNull()"}`;
315
543
  }
316
- return `timestamp("${field.column}")${field.nullable ? "" : ".notNull()"}`;
544
+ return `timestamp("${field.column}", { withTimezone: true, mode: "date" })${field.nullable ? "" : ".notNull()"}`;
317
545
  }
318
- function sqlType(field, dialect) {
546
+ function sqlType(field, dialect, options = {}) {
319
547
  if (field.kind === "id") {
320
548
  return dialect === "mysql" ? "varchar(191)" : "text";
321
549
  }
322
550
  if (field.kind === "string") {
323
- return dialect === "mysql" && (field.unique || field.references) ? "varchar(191)" : "text";
551
+ return dialect === "mysql" && (field.unique || field.references || options.indexed) ? "varchar(191)" : "text";
324
552
  }
325
553
  if (field.kind === "boolean") {
326
554
  return dialect === "sqlite" ? "integer" : "boolean";
@@ -331,7 +559,7 @@ function sqlType(field, dialect) {
331
559
  if (dialect === "sqlite") {
332
560
  return "text";
333
561
  }
334
- return "timestamp";
562
+ return "timestamptz";
335
563
  }
336
564
  function sqlIdentifier(dialect, value) {
337
565
  if (dialect === "mysql") {
@@ -339,6 +567,14 @@ function sqlIdentifier(dialect, value) {
339
567
  }
340
568
  return `"${value}"`;
341
569
  }
570
+ function sqlCreateIndexStatement(dialect, table, constraint) {
571
+ const indexName = sqlIdentifier(dialect, constraint.name);
572
+ const tableName = sqlIdentifier(dialect, table);
573
+ const columns = constraint.columns.map((column) => sqlIdentifier(dialect, column)).join(", ");
574
+ const createKeyword = constraint.unique ? "create unique index" : "create index";
575
+ const ifNotExists = dialect === "mysql" ? "" : " if not exists";
576
+ return `${createKeyword}${ifNotExists} ${indexName} on ${tableName}(${columns});`;
577
+ }
342
578
  function renderPrismaSchema(schema, options = {}) {
343
579
  const manifest = createManifest(schema);
344
580
  const provider = options.provider ?? "postgresql";
@@ -421,10 +657,18 @@ function renderPrismaSchema(schema, options = {}) {
421
657
  relation.many ? ` ${relationName} ${capitalize(relation.sourceModel)}[]` : ` ${relationName} ${capitalize(relation.sourceModel)}?`
422
658
  );
423
659
  }
424
- const mapLine = model2.table !== modelName ? `
425
- @@map("${model2.table}")` : "";
660
+ const modelLines = [
661
+ ...lines,
662
+ ...model2.constraints.unique.map(
663
+ (constraint) => ` @@unique([${constraint.fields.join(", ")}])`
664
+ ),
665
+ ...model2.constraints.indexes.map(
666
+ (constraint) => ` @@index([${constraint.fields.join(", ")}])`
667
+ ),
668
+ ...model2.table !== modelName ? [` @@map("${model2.table}")`] : []
669
+ ];
426
670
  return `model ${modelName} {
427
- ${lines.join("\n")}${mapLine}
671
+ ${modelLines.join("\n")}
428
672
  }`;
429
673
  });
430
674
  return `generator ${generatorName} {
@@ -444,17 +688,33 @@ function renderDrizzleSchema(schema, options) {
444
688
  const coreImports = drizzleImports(options.dialect, manifest).join(", ");
445
689
  const tableFactory = options.dialect === "pg" ? "pgTable" : options.dialect === "mysql" ? "mysqlTable" : "sqliteTable";
446
690
  const modelBlocks = Object.values(manifest.models).map((model2) => {
691
+ const indexedFields = constrainedFields(model2);
447
692
  const lines = Object.values(model2.fields).map((field) => {
448
- let value = drizzleColumn(field, options.dialect);
693
+ let value = drizzleColumn(field, options.dialect, { indexed: indexedFields.has(field.name) });
449
694
  if (field.references) {
450
695
  const [targetModel, targetField] = field.references.split(".");
451
696
  value += `.references(() => ${targetModel}.${targetField})`;
452
697
  }
453
698
  return ` ${field.name}: ${value}`;
454
699
  });
455
- return `export const ${model2.name} = ${tableFactory}("${model2.table}", {
700
+ const constraintLines = [
701
+ ...model2.constraints.unique.map(
702
+ (constraint) => ` ${drizzleConstraintProperty(constraint)}: uniqueIndex("${constraint.name}").on(${constraint.fields.map((fieldName) => `table.${fieldName}`).join(", ")})`
703
+ ),
704
+ ...model2.constraints.indexes.map(
705
+ (constraint) => ` ${drizzleConstraintProperty(constraint)}: index("${constraint.name}").on(${constraint.fields.map((fieldName) => `table.${fieldName}`).join(", ")})`
706
+ )
707
+ ];
708
+ if (!constraintLines.length) {
709
+ return `export const ${model2.name} = ${tableFactory}("${model2.table}", {
456
710
  ${lines.join(",\n")}
457
711
  });`;
712
+ }
713
+ return `export const ${model2.name} = ${tableFactory}("${model2.table}", {
714
+ ${lines.join(",\n")}
715
+ }, (table) => ({
716
+ ${constraintLines.join(",\n")}
717
+ }));`;
458
718
  });
459
719
  const relationBlocks = Object.values(manifest.models).map((model2) => {
460
720
  const lines = Object.entries(model2.relations).flatMap(([relationName, relation]) => {
@@ -493,10 +753,13 @@ ${[...modelBlocks, ...relationBlocks].join("\n\n")}
493
753
  }
494
754
  function renderSafeSql(schema, options) {
495
755
  const manifest = createManifest(schema);
496
- const statements = Object.values(manifest.models).map((model2) => {
756
+ const statements = Object.values(manifest.models).flatMap((model2) => {
757
+ const indexedFields = constrainedFields(model2);
497
758
  const columns = Object.values(model2.fields).map((field) => {
498
759
  const parts = [
499
- `${sqlIdentifier(options.dialect, field.column)} ${sqlType(field, options.dialect)}`
760
+ `${sqlIdentifier(options.dialect, field.column)} ${sqlType(field, options.dialect, {
761
+ indexed: indexedFields.has(field.name)
762
+ })}`
500
763
  ];
501
764
  if (field.kind === "id") parts.push("primary key");
502
765
  if (!field.nullable) parts.push("not null");
@@ -519,9 +782,17 @@ function renderSafeSql(schema, options) {
519
782
  }
520
783
  return ` ${parts.join(" ")}`;
521
784
  });
522
- return `create table if not exists ${sqlIdentifier(options.dialect, model2.table)} (
785
+ return [
786
+ `create table if not exists ${sqlIdentifier(options.dialect, model2.table)} (
523
787
  ${columns.join(",\n")}
524
- );`;
788
+ );`,
789
+ ...model2.constraints.unique.map(
790
+ (constraint) => sqlCreateIndexStatement(options.dialect, model2.table, constraint)
791
+ ),
792
+ ...model2.constraints.indexes.map(
793
+ (constraint) => sqlCreateIndexStatement(options.dialect, model2.table, constraint)
794
+ )
795
+ ];
525
796
  });
526
797
  return `${statements.join("\n\n")}
527
798
  `;
@@ -545,6 +816,14 @@ ${block}
545
816
  // src/memory.ts
546
817
  var import_node_crypto = require("crypto");
547
818
  var isDate = (value) => value instanceof Date;
819
+ var manifestCache = /* @__PURE__ */ new WeakMap();
820
+ function getManifest(schema) {
821
+ const cached = manifestCache.get(schema);
822
+ if (cached) return cached;
823
+ const next = createManifest(schema);
824
+ manifestCache.set(schema, next);
825
+ return next;
826
+ }
548
827
  function evaluateFilter(value, filter) {
549
828
  if (filter === void 0 || filter === null || typeof filter !== "object" || isDate(filter) || Array.isArray(filter)) {
550
829
  return value === filter;
@@ -737,6 +1016,11 @@ function createMemoryDriver(seed) {
737
1016
  return projectRow(schema, model2, row, args.select);
738
1017
  },
739
1018
  async findUnique(schema, model2, args) {
1019
+ requireUniqueLookup(
1020
+ getManifest(schema).models[model2],
1021
+ args.where,
1022
+ "FindUnique"
1023
+ );
740
1024
  const row = applyQuery(getRows(model2), args)[0];
741
1025
  if (!row) return null;
742
1026
  return projectRow(schema, model2, row, args.select);
@@ -768,12 +1052,32 @@ function createMemoryDriver(seed) {
768
1052
  return rows.length;
769
1053
  },
770
1054
  async upsert(schema, model2, args) {
1055
+ const lookup = requireUniqueLookup(
1056
+ getManifest(schema).models[model2],
1057
+ args.where,
1058
+ "Upsert"
1059
+ );
1060
+ validateUniqueLookupUpdateData(
1061
+ getManifest(schema).models[model2],
1062
+ args.update,
1063
+ lookup,
1064
+ "Upsert"
1065
+ );
771
1066
  const row = getRows(model2).find((item) => matchesWhere(item, args.where));
772
1067
  if (row) {
773
1068
  Object.assign(row, args.update);
774
1069
  return projectRow(schema, model2, row, args.select);
775
1070
  }
776
- const created = buildRow(schema, model2, args.create);
1071
+ const created = buildRow(
1072
+ schema,
1073
+ model2,
1074
+ mergeUniqueLookupCreateData(
1075
+ getManifest(schema).models[model2],
1076
+ args.create,
1077
+ lookup,
1078
+ "Upsert"
1079
+ )
1080
+ );
777
1081
  getRows(model2).push(created);
778
1082
  return projectRow(schema, model2, created, args.select);
779
1083
  },
@@ -845,6 +1149,7 @@ function model(config) {
845
1149
  table: config.table,
846
1150
  fields: config.fields,
847
1151
  relations: config.relations ?? {},
1152
+ constraints: config.constraints ?? {},
848
1153
  description: config.description
849
1154
  };
850
1155
  }
@@ -868,11 +1173,16 @@ function defineSchema(models) {
868
1173
  hasOne,
869
1174
  id,
870
1175
  manyToMany,
1176
+ mergeUniqueLookupCreateData,
871
1177
  model,
872
1178
  renderDrizzleSchema,
873
1179
  renderPrismaSchema,
874
1180
  renderSafeSql,
875
1181
  replaceGeneratedBlock,
876
- string
1182
+ requireUniqueLookup,
1183
+ resolveRowIdentityLookup,
1184
+ string,
1185
+ toUniqueLookupWhere,
1186
+ validateUniqueLookupUpdateData
877
1187
  });
878
1188
  //# sourceMappingURL=index.cjs.map