@easy-cms/drizzle 0.4.0 → 0.6.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/dist/index.d.ts CHANGED
@@ -75,6 +75,10 @@ interface ColumnModel {
75
75
  readonly path: readonly string[];
76
76
  readonly field: Field;
77
77
  readonly column: string;
78
+ /** For localized fields: the locale whose value this column holds. */
79
+ readonly locale?: string;
80
+ /** The default locale's column, used when a query names no locale. */
81
+ readonly defaultLocale?: boolean;
78
82
  }
79
83
  /** A child table holding array rows or hasMany values. */
80
84
  interface ChildModel {
@@ -82,6 +86,9 @@ interface ChildModel {
82
86
  readonly path: readonly string[];
83
87
  readonly field: Field;
84
88
  readonly table: TableModel;
89
+ /** Localized: rows carry `_locale`, and the data holds `{ [locale]: rows }`. */
90
+ readonly locales?: readonly string[];
91
+ readonly defaultLocale?: string;
85
92
  }
86
93
  interface TableModel {
87
94
  readonly name: string;
package/dist/index.js CHANGED
@@ -22,15 +22,21 @@ function setAt(data, path, value) {
22
22
  }
23
23
  current[path[path.length - 1]] = value;
24
24
  }
25
+ var columnPath = (column) => column.locale === void 0 ? column.path : [...column.path, column.locale];
25
26
  function rowToData(model, row) {
26
27
  const data = {};
27
- for (const column of model.columns) setAt(data, column.path, row[column.column] ?? null);
28
- for (const child of model.children) setAt(data, child.path, []);
28
+ for (const column of model.columns) setAt(data, columnPath(column), row[column.column] ?? null);
29
+ for (const child of model.children)
30
+ setAt(
31
+ data,
32
+ child.path,
33
+ child.locales ? Object.fromEntries(child.locales.map((locale) => [locale, []])) : []
34
+ );
29
35
  return data;
30
36
  }
31
37
  function dataToRow(model, data) {
32
38
  const row = {};
33
- for (const column of model.columns) row[column.column] = getAt(data, column.path) ?? null;
39
+ for (const column of model.columns) row[column.column] = getAt(data, columnPath(column)) ?? null;
34
40
  return row;
35
41
  }
36
42
  var CHUNK = 500;
@@ -54,8 +60,12 @@ async function attachChildren(db, model, rows, docs) {
54
60
  childRows.forEach((childRow, i) => {
55
61
  const index = indexById.get(childRow._parent_id);
56
62
  if (index === void 0) return;
57
- const list = getAt(docs[index], child.path);
58
- list.push(child.kind === "array" ? { id: childRow.id, ...childDocs?.[i] } : childRow.value);
63
+ const locale = child.locales ? String(childRow._locale) : void 0;
64
+ const path = locale === void 0 ? child.path : [...child.path, locale];
65
+ const list = getAt(docs[index], path);
66
+ if (!Array.isArray(list)) return;
67
+ const id = locale === void 0 ? childRow.id : String(childRow.id).replace(new RegExp(`:${locale}$`), "");
68
+ list.push(child.kind === "array" ? { id, ...childDocs?.[i] } : childRow.value);
59
69
  });
60
70
  }
61
71
  }
@@ -76,20 +86,41 @@ function toDocument(row, data, drafts) {
76
86
  async function insertChildren(db, model, parentId, data) {
77
87
  for (const child of model.children) {
78
88
  const value = getAt(data, child.path);
89
+ if (child.locales) {
90
+ if (!value || typeof value !== "object") continue;
91
+ for (const locale of child.locales) {
92
+ const items = value[locale];
93
+ if (Array.isArray(items) && items.length > 0)
94
+ await insertChild(db, child, parentId, items, locale);
95
+ }
96
+ continue;
97
+ }
79
98
  if (!Array.isArray(value) || value.length === 0) continue;
80
99
  await insertChild(db, child, parentId, value);
81
100
  }
82
101
  }
83
- async function insertChild(db, child, parentId, items) {
102
+ async function insertChild(db, child, parentId, items, locale) {
103
+ const localeColumn = locale === void 0 ? {} : { _locale: locale };
84
104
  if (child.kind === "values") {
85
- const rows = items.map((value, i) => ({ _parent_id: parentId, _order: i, value }));
105
+ const rows = items.map((value, i) => ({
106
+ _parent_id: parentId,
107
+ _order: i,
108
+ value,
109
+ ...localeColumn
110
+ }));
86
111
  for (const part of chunks(rows)) await db.insert(child.table.table).values(part);
87
112
  return;
88
113
  }
89
114
  for (const [i, item] of items.entries()) {
90
115
  const rowData = item;
91
- const id = rowData.id;
92
- await db.insert(child.table.table).values({ ...dataToRow(child.table, rowData), id, _parent_id: parentId, _order: i });
116
+ const id = locale === void 0 ? rowData.id : `${rowData.id}:${locale}`;
117
+ await db.insert(child.table.table).values({
118
+ ...dataToRow(child.table, rowData),
119
+ id,
120
+ _parent_id: parentId,
121
+ _order: i,
122
+ ...localeColumn
123
+ });
93
124
  await insertChildren(db, child.table, id, rowData);
94
125
  }
95
126
  }
@@ -412,14 +443,31 @@ function resolvePath(model, segments, drafts) {
412
443
  continue;
413
444
  }
414
445
  const child = model.children.find((c) => c.path.join(".") === path.join("."));
446
+ if (child?.locales) {
447
+ const next = segments[i + 1];
448
+ const named = next !== void 0 && child.locales.includes(next);
449
+ return {
450
+ kind: "child",
451
+ child,
452
+ rest: segments.slice(i + (named ? 2 : 1)),
453
+ locale: named ? next : child.defaultLocale
454
+ };
455
+ }
415
456
  if (child) return { kind: "child", child, rest: segments.slice(i + 1) };
416
- if (i < segments.length - 1) {
457
+ const locale = field.localized ? segments[i + 1] : void 0;
458
+ const last = field.localized && locale !== void 0 ? i + 1 : i;
459
+ if (last < segments.length - 1) {
417
460
  throw new QueryError(`Cannot query inside "${shown}" (${field.type} field)`);
418
461
  }
419
- if (field.type === "json" || field.type === "richText") {
462
+ if (field.type === "json" || field.type === "richText" || field.type === "blocks") {
420
463
  throw new QueryError(`Cannot query "${shown}": ${field.type} fields are not queryable`);
421
464
  }
422
- const column = model.columns.find((c) => c.path.join(".") === path.join("."));
465
+ const same = (c) => c.path.join(".") === path.join(".");
466
+ const column = field.localized ? model.columns.find(
467
+ (c) => same(c) && (locale === void 0 ? c.defaultLocale : c.locale === locale)
468
+ ) : model.columns.find(same);
469
+ if (field.localized && !column)
470
+ throw new QueryError(`Unknown locale "${locale}" for "${shown}"`);
423
471
  if (!column) throw new QueryError(`Unknown field "${shown}"`);
424
472
  return { kind: "column", column: col2(model, column.column), field };
425
473
  }
@@ -510,14 +558,15 @@ var WhereBuilder = class {
510
558
  if (!OPERATORS.has(op)) throw new QueryError(`Unknown operator "${op}" on "${path}"`);
511
559
  if (target.kind === "column")
512
560
  return operatorSQL(this.dialect, target.column, target.field, op, value, path);
513
- return this.child(model, target.child, target.rest, op, value, path);
561
+ return this.child(model, target.child, target.rest, op, value, path, target.locale);
514
562
  });
515
563
  return parts.length === 1 ? parts[0] : and(...parts);
516
564
  }
517
565
  /** Conditions on array rows or hasMany values become (NOT) EXISTS subqueries. */
518
- child(parent, child, rest, op, value, path) {
566
+ child(parent, child, rest, op, value, path, locale) {
519
567
  const table = child.table;
520
- const link = eq2(col2(table, "_parent_id"), col2(parent, "id"));
568
+ const parentLink = eq2(col2(table, "_parent_id"), col2(parent, "id"));
569
+ const link = locale === void 0 ? parentLink : and(parentLink, eq2(col2(table, "_locale"), locale));
521
570
  const subquery = (condition) => exists(
522
571
  this.db.select({ one: sql`1` }).from(table.table).where(condition ? and(link, condition) : link)
523
572
  );
@@ -569,6 +618,8 @@ function columnKind(field) {
569
618
  return "boolean";
570
619
  case "json":
571
620
  case "richText":
621
+ // Blocks are stored whole as JSON: rows of different kinds don't fit one child table.
622
+ case "blocks":
572
623
  return "json";
573
624
  case "upload":
574
625
  case "relationship":
@@ -583,7 +634,8 @@ function buildSchema(config, prefix, dialect) {
583
634
  prefix,
584
635
  tableNames: /* @__PURE__ */ new Set([migrationsTableName(prefix)]),
585
636
  // The hash covers the logical schema only, so it stays stable across releases for the same config.
586
- description: []
637
+ description: [],
638
+ localization: config.localization
587
639
  };
588
640
  const collections = /* @__PURE__ */ new Map();
589
641
  const tables = {};
@@ -688,6 +740,14 @@ function buildTable(builder, name, kind, fields, options) {
688
740
  );
689
741
  add("_order", dialect.integer("_order").notNull(), "int!");
690
742
  indexes.push({ column: "_parent_id", unique: false });
743
+ if (options.localized) {
744
+ add(
745
+ "_locale",
746
+ dialect.text("_locale").notNull().default(options.localized),
747
+ `locale:${options.localized}`
748
+ );
749
+ indexes.push({ column: "_locale", unique: false });
750
+ }
691
751
  if (kind === "values" && options.valueField) {
692
752
  const valueKind = columnKind(options.valueField);
693
753
  add("value", makeColumn(dialect, "value", valueKind), valueKind);
@@ -701,26 +761,57 @@ function buildTable(builder, name, kind, fields, options) {
701
761
  if (field.type === "group") {
702
762
  walk(field.fields, fieldPath, false);
703
763
  } else if (field.type === "array") {
764
+ const localization = field.localized ? builder.localization : null;
704
765
  const child = buildTable(builder, `${name}__${base}`, "array", field.fields, {
705
- parentIdKind: kind === "array" ? "text" : "integer"
766
+ parentIdKind: kind === "array" ? "text" : "integer",
767
+ ...localization ? { localized: localization.defaultLocale } : {}
768
+ });
769
+ children.push({
770
+ kind: "array",
771
+ path: fieldPath,
772
+ field,
773
+ table: child,
774
+ ...localeInfo(localization)
706
775
  });
707
- children.push({ kind: "array", path: fieldPath, field, table: child });
708
776
  described.push([base, "array", child.name]);
709
777
  } else if ((field.type === "select" || field.type === "relationship") && field.hasMany) {
778
+ const localization = field.localized ? builder.localization : null;
710
779
  const child = buildTable(builder, `${name}__${base}`, "values", [], {
711
780
  parentIdKind: kind === "array" ? "text" : "integer",
712
- valueField: field
781
+ valueField: field,
782
+ ...localization ? { localized: localization.defaultLocale } : {}
783
+ });
784
+ children.push({
785
+ kind: "values",
786
+ path: fieldPath,
787
+ field,
788
+ table: child,
789
+ ...localeInfo(localization)
713
790
  });
714
- children.push({ kind: "values", path: fieldPath, field, table: child });
715
791
  described.push([base, "values", child.name]);
716
792
  } else {
717
793
  const kindOfColumn = columnKind(field);
718
- add(base, makeColumn(dialect, base, kindOfColumn), kindOfColumn);
719
- columnModels.push({ path: fieldPath, field, column: base });
720
- if (topLevel && kind === "root" && (field.unique || field.type === "slug")) {
721
- indexes.push({ column: base, unique: true });
722
- } else if (field.index || field.unique || field.type === "relationship" || field.type === "upload") {
723
- indexes.push({ column: base, unique: false });
794
+ const localization = field.localized ? builder.localization : null;
795
+ const targets = localization ? localization.locales.map((locale) => ({
796
+ column: locale === localization.defaultLocale ? base : `${base}__${snake(locale)}`,
797
+ locale
798
+ })) : [{ column: base, locale: void 0 }];
799
+ for (const { column, locale } of targets) {
800
+ add(column, makeColumn(dialect, column, kindOfColumn), kindOfColumn);
801
+ columnModels.push(
802
+ locale === void 0 ? { path: fieldPath, field, column } : {
803
+ path: fieldPath,
804
+ field,
805
+ column,
806
+ locale,
807
+ defaultLocale: locale === localization?.defaultLocale
808
+ }
809
+ );
810
+ if (topLevel && kind === "root" && (field.unique || field.type === "slug")) {
811
+ indexes.push({ column, unique: true });
812
+ } else if (field.index || field.unique || field.type === "relationship" || field.type === "upload") {
813
+ indexes.push({ column, unique: false });
814
+ }
724
815
  }
725
816
  }
726
817
  }
@@ -738,6 +829,9 @@ function buildTable(builder, name, kind, fields, options) {
738
829
  const model = { name, kind, table, fields, columns: columnModels, children };
739
830
  return options.valueField ? { ...model, valueField: options.valueField } : model;
740
831
  }
832
+ function localeInfo(localization) {
833
+ return localization ? { locales: localization.locales, defaultLocale: localization.defaultLocale } : {};
834
+ }
741
835
 
742
836
  // src/database.ts
743
837
  var DEFAULT_TABLE_PREFIX = "ecms_";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/drizzle",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Shared Drizzle ORM layer for Easy CMS database adapters",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,10 +23,10 @@
23
23
  "drizzle-orm": "0.45.3"
24
24
  },
25
25
  "peerDependencies": {
26
- "@easy-cms/core": "^0.4.0"
26
+ "@easy-cms/core": "^0.6.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@easy-cms/core": "^0.4.0"
29
+ "@easy-cms/core": "^0.6.0"
30
30
  },
31
31
  "publishConfig": {
32
32
  "access": "public"