@easy-cms/drizzle 1.7.1 → 1.9.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
@@ -214,4 +214,5 @@ declare class Migrator {
214
214
  private files;
215
215
  }
216
216
 
217
- export { type AnyColumn, type AnyTable, type ColumnBuilder, type Connection, DEFAULT_MIGRATION_DIR, DEFAULT_TABLE_PREFIX, type Dialect, type DrizzleAdapterOptions, type DrizzleDb, type IndexBuilder, type JsonElement, type JsonValueType, Migrator, type SchemaModel, type Snapshot, type SqlRunner, type Statement, buildSchema, connectDatabase, snake, unionSnapshot };
217
+ export { DEFAULT_MIGRATION_DIR, DEFAULT_TABLE_PREFIX, Migrator, buildSchema, connectDatabase, snake, unionSnapshot };
218
+ export type { AnyColumn, AnyTable, ColumnBuilder, Connection, Dialect, DrizzleAdapterOptions, DrizzleDb, IndexBuilder, JsonElement, JsonValueType, SchemaModel, Snapshot, SqlRunner, Statement };
package/dist/index.js CHANGED
@@ -1031,6 +1031,49 @@ var WhereBuilder = class {
1031
1031
  order.push(desc(col2(model, "id")));
1032
1032
  return order;
1033
1033
  }
1034
+ /**
1035
+ * The columns a cursor goes by, in `orderBy`'s order: the sort's, then `id`. Only plain
1036
+ * columns: a sort on a value inside blocks has nothing to compare a cursor with.
1037
+ */
1038
+ keyset(model, sort) {
1039
+ const keys = sort.map((entry) => {
1040
+ const descending = entry.startsWith("-");
1041
+ const path = descending ? entry.slice(1) : entry;
1042
+ const target = resolvePath(model, path.split("."), this.drafts, this.localization);
1043
+ if (target.kind !== "column")
1044
+ throw new QueryError(
1045
+ `A cursor can't follow a sort by "${path}"; sort by a field of the document, or use page`
1046
+ );
1047
+ return { column: target.column, descending };
1048
+ });
1049
+ keys.push({ column: col2(model, "id"), descending: true });
1050
+ return keys;
1051
+ }
1052
+ /**
1053
+ * Rows after `values` (the keys of the last row of the page before) in the keyset's order.
1054
+ * Where nulls go follows the database's default order, as `orderBy` leaves it: last in
1055
+ * ascending order on Postgres, first on SQLite.
1056
+ */
1057
+ after(keys, values) {
1058
+ const nullsHigh = this.dialect.name === "postgres";
1059
+ const equal = (key, value) => value === null ? isNull(key.column) : eq2(key.column, value);
1060
+ const later = (key, value) => {
1061
+ const nullsLater = nullsHigh !== key.descending;
1062
+ if (value === null) return nullsLater ? sql`1 = 0` : isNotNull(key.column);
1063
+ const beyond = key.descending ? lt(key.column, value) : gt(key.column, value);
1064
+ return nullsLater ? or(beyond, isNull(key.column)) : beyond;
1065
+ };
1066
+ const branches = keys.map(
1067
+ (key, i) => and2(...keys.slice(0, i).map((k, j) => equal(k, values[j])), later(key, values[i]))
1068
+ );
1069
+ const rest = or(...branches);
1070
+ const [first] = keys;
1071
+ const start = values[0];
1072
+ if (!first || start === null || start === void 0) return rest;
1073
+ const from = first.descending ? lte(first.column, start) : gte(first.column, start);
1074
+ const bound = nullsHigh !== first.descending ? or(from, isNull(first.column)) : from;
1075
+ return and2(bound, rest);
1076
+ }
1034
1077
  };
1035
1078
 
1036
1079
  // src/database.ts
@@ -1052,7 +1095,8 @@ async function connectDatabase(dialect, connection, args, options = {}) {
1052
1095
  });
1053
1096
  if (args.schema === "push") await migrator.push();
1054
1097
  else if (args.schema === "verify") await migrator.verify();
1055
- return new DrizzleDatabase(dialect, connection, schema, migrator, prefix);
1098
+ const countCacheMs = (args.config.countCacheSeconds ?? 5) * 1e3;
1099
+ return new DrizzleDatabase(dialect, connection, schema, migrator, prefix, countCacheMs);
1056
1100
  } catch (error) {
1057
1101
  await connection.close().catch(() => {
1058
1102
  });
@@ -1060,12 +1104,13 @@ async function connectDatabase(dialect, connection, args, options = {}) {
1060
1104
  }
1061
1105
  }
1062
1106
  var DrizzleDatabase = class {
1063
- constructor(dialect, connection, schema, migrator, tablePrefix) {
1107
+ constructor(dialect, connection, schema, migrator, tablePrefix, countCacheMs = 0) {
1064
1108
  this.dialect = dialect;
1065
1109
  this.connection = connection;
1066
1110
  this.schema = schema;
1067
1111
  this.migrator = migrator;
1068
1112
  this.tablePrefix = tablePrefix;
1113
+ this.countCacheMs = countCacheMs;
1069
1114
  this.db = connection.db;
1070
1115
  for (const [slug, model] of schema.collections) {
1071
1116
  this.builders.set(
@@ -1079,7 +1124,14 @@ var DrizzleDatabase = class {
1079
1124
  schema;
1080
1125
  migrator;
1081
1126
  tablePrefix;
1127
+ countCacheMs;
1082
1128
  builders = /* @__PURE__ */ new Map();
1129
+ /**
1130
+ * Totals of recent paginated finds, by collection and query, for `countCacheSeconds`: counting
1131
+ * can't use an index, and many people open the same lists. Writes don't clear them (on a busy
1132
+ * site they would all the time), so a total is up to that old, on every instance alike.
1133
+ */
1134
+ counts = /* @__PURE__ */ new Map();
1083
1135
  db;
1084
1136
  model(slug) {
1085
1137
  const model = this.schema.collections.get(slug);
@@ -1097,13 +1149,8 @@ var DrizzleDatabase = class {
1097
1149
  return rows.map((row, i) => toDocument(row, docs[i] ?? {}, model.config.drafts === true));
1098
1150
  }
1099
1151
  async find(args) {
1100
- const model = this.model(args.collection);
1101
- const condition = this.where(args.collection, args.where);
1102
- const orderBy = this.builder(args.collection).orderBy(model.root, args.sort);
1103
- let query = this.db.select().from(model.root.table).where(condition).orderBy(...orderBy).$dynamic();
1104
- if (args.limit > 0) query = query.limit(args.limit).offset((args.page - 1) * args.limit);
1105
- const rows = await query;
1106
- const totalDocs = args.limit > 0 && (args.page > 1 || rows.length === args.limit) ? await this.count({ collection: args.collection, where: args.where }) : (args.page - 1) * args.limit + rows.length;
1152
+ const { model, rows } = await this.findRows(args, args.limit);
1153
+ const totalDocs = args.limit > 0 && (args.page > 1 || rows.length === args.limit) ? await this.cachedCount(args.collection, args.where) : (args.page - 1) * args.limit + rows.length;
1107
1154
  const totalPages = args.limit > 0 ? Math.max(1, Math.ceil(totalDocs / args.limit)) : 1;
1108
1155
  return {
1109
1156
  docs: await this.load(model, rows),
@@ -1115,6 +1162,45 @@ var DrizzleDatabase = class {
1115
1162
  hasPrevPage: args.page > 1
1116
1163
  };
1117
1164
  }
1165
+ async findPage(args) {
1166
+ const { model, rows } = await this.findRows(args, args.limit + 1);
1167
+ const more = rows.length > args.limit;
1168
+ return {
1169
+ docs: await this.load(model, more ? rows.slice(0, args.limit) : rows),
1170
+ totalDocs: null,
1171
+ limit: args.limit,
1172
+ page: args.page,
1173
+ totalPages: null,
1174
+ hasNextPage: more,
1175
+ hasPrevPage: args.page > 1
1176
+ };
1177
+ }
1178
+ async findAfter(args) {
1179
+ const model = this.model(args.collection);
1180
+ const builder = this.builder(args.collection);
1181
+ const keys = builder.keyset(model.root, args.sort);
1182
+ const condition = this.where(args.collection, args.where);
1183
+ const after = args.after ? builder.after(keys, args.after) : void 0;
1184
+ const columns = Object.entries(getTableColumns(model.root.table));
1185
+ const names = keys.map(({ column }) => columns.find(([, c]) => c === column)?.[0]);
1186
+ const rows = await this.db.select().from(model.root.table).where(and3(condition, after)).orderBy(...builder.orderBy(model.root, args.sort)).limit(args.limit + 1);
1187
+ const more = rows.length > args.limit;
1188
+ const page = more ? rows.slice(0, args.limit) : rows;
1189
+ const last = page.at(-1);
1190
+ return {
1191
+ docs: await this.load(model, page),
1192
+ hasNextPage: more,
1193
+ next: last ? names.map((name) => last[name] ?? null) : null
1194
+ };
1195
+ }
1196
+ async findRows(args, take) {
1197
+ const model = this.model(args.collection);
1198
+ const condition = this.where(args.collection, args.where);
1199
+ const orderBy = this.builder(args.collection).orderBy(model.root, args.sort);
1200
+ let query = this.db.select().from(model.root.table).where(condition).orderBy(...orderBy).$dynamic();
1201
+ if (args.limit > 0) query = query.limit(take).offset((args.page - 1) * args.limit);
1202
+ return { model, rows: await query };
1203
+ }
1118
1204
  async findById(args) {
1119
1205
  const [doc] = await this.findByIds({ collection: args.collection, ids: [args.id] });
1120
1206
  return doc ?? null;
@@ -1125,6 +1211,18 @@ var DrizzleDatabase = class {
1125
1211
  const rows = await this.db.select().from(model.root.table).where(inArray3(model.root.table.id, [...args.ids]));
1126
1212
  return this.load(model, rows);
1127
1213
  }
1214
+ /** A paginated find's total, from `counts` while it is fresh. */
1215
+ cachedCount(collection, where) {
1216
+ if (this.countCacheMs <= 0) return this.count({ collection, where });
1217
+ const key = `${collection}\0${JSON.stringify(where ?? null)}`;
1218
+ const now = Date.now();
1219
+ const cached = this.counts.get(key);
1220
+ if (cached && cached.until > now) return cached.total;
1221
+ const total = this.count({ collection, where });
1222
+ this.counts.set(key, { total, until: now + this.countCacheMs });
1223
+ total.catch(() => this.counts.delete(key));
1224
+ return total;
1225
+ }
1128
1226
  async count(args) {
1129
1227
  const model = this.model(args.collection);
1130
1228
  const [row] = await this.db.select({ total: count() }).from(model.root.table).where(this.where(args.collection, args.where));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/drizzle",
3
- "version": "1.7.1",
3
+ "version": "1.9.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.4"
24
24
  },
25
25
  "peerDependencies": {
26
- "@easy-cms/core": "^1.7.1"
26
+ "@easy-cms/core": "^1.9.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@easy-cms/core": "^1.7.1"
29
+ "@easy-cms/core": "^1.9.0"
30
30
  },
31
31
  "publishConfig": {
32
32
  "access": "public"