@spfn/core 0.3.0-beta.32 → 0.3.0-beta.34

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/db/index.js CHANGED
@@ -5,11 +5,11 @@ import net from 'net';
5
5
  import postgres from 'postgres';
6
6
  import { QueryError, ConnectionError, DeadlockError, TransactionError, ConstraintViolationError, DuplicateEntryError, SerializableError } from '@spfn/core/errors';
7
7
  import { parseNumber, parseBoolean } from '@spfn/core/env';
8
+ import { sql, count as count$1, lt, gt, and, desc, asc, getTableName, eq, isTable, getColumns, is, View } from 'drizzle-orm';
8
9
  import { existsSync, readdirSync, readFileSync, statSync, realpathSync, lstatSync } from 'fs';
9
10
  import { join, relative } from 'path';
10
11
  import mm from 'micromatch';
11
- import { sql, count as count$1, lt, gt, and, desc, asc, eq } from 'drizzle-orm';
12
- import { bigserial, timestamp, bigint, uuid as uuid$1, text, jsonb, pgSchema } from 'drizzle-orm/pg-core';
12
+ import { bigserial, bigint, timestamp, uuid as uuid$1, text, jsonb, pgSchema } from 'drizzle-orm/pg-core';
13
13
  import { createHash, randomUUID } from 'crypto';
14
14
  import { AsyncLocalStorage } from 'async_hooks';
15
15
  import { createMiddleware } from 'hono/factory';
@@ -397,9 +397,95 @@ function buildMonitoringConfig(options) {
397
397
  logQueries: options?.logQueries ?? parseEnvBoolean("DB_MONITORING_LOG_QUERIES", false)
398
398
  };
399
399
  }
400
+ var TABLE_SCHEMA = Symbol.for("drizzle:Schema");
401
+ var tenantColumnConfigs = /* @__PURE__ */ new WeakSet();
402
+ var tenantColumnByTable = /* @__PURE__ */ new WeakMap();
403
+ var tenantTables = /* @__PURE__ */ new Map();
404
+ function registerTenantColumn(table, column) {
405
+ const name = getTableName(table);
406
+ if (tenantColumnByTable.has(table)) {
407
+ throw new Error(`Table "${name}" declares more than one tenantKey() column`);
408
+ }
409
+ const schema = table[TABLE_SCHEMA];
410
+ tenantColumnByTable.set(table, column);
411
+ tenantColumnConfigs.add(configOf(column));
412
+ tenantTables.set(`${schema ?? ""}.${name}`, { schema, name, columnName: column.name });
413
+ }
414
+ function findTenantColumn(table) {
415
+ if (!isTable(table)) {
416
+ return void 0;
417
+ }
418
+ const entry = Object.entries(getColumns(table)).find(([, column]) => tenantColumnConfigs.has(configOf(column)));
419
+ return entry ? { key: entry[0], column: entry[1] } : void 0;
420
+ }
421
+ function listTenantTables() {
422
+ return [...tenantTables.values()];
423
+ }
424
+ function configOf(column) {
425
+ return column.config;
426
+ }
427
+
428
+ // src/db/tenant/query-warning.ts
429
+ var WARNING_ENVIRONMENTS = /* @__PURE__ */ new Set(["local", "development"]);
430
+ function createDevelopmentQueryLogger(nodeEnv, warn) {
431
+ return nodeEnv && WARNING_ENVIRONMENTS.has(nodeEnv) ? createTenantQueryLogger({ warn }) : void 0;
432
+ }
433
+ function createTenantQueryLogger(options) {
434
+ const warned = /* @__PURE__ */ new Set();
435
+ const tables = options.tables ?? listTenantTables;
436
+ function warnIfUnscoped(query) {
437
+ if (warned.has(query)) {
438
+ return;
439
+ }
440
+ const unscoped = findUnscopedTenantTables(query, tables());
441
+ if (unscoped.length > 0) {
442
+ warned.add(query);
443
+ options.warn("Query on a tenant-aware table without a tenant condition", { tables: unscoped, sql: query });
444
+ }
445
+ }
446
+ return {
447
+ logQuery(query) {
448
+ try {
449
+ warnIfUnscoped(query);
450
+ } catch {
451
+ }
452
+ }
453
+ };
454
+ }
455
+ function findUnscopedTenantTables(sql5, tables) {
456
+ if (/^\s*insert\b/i.test(sql5)) {
457
+ return [];
458
+ }
459
+ return tables.flatMap(
460
+ (table) => referencesOf(sql5, table).filter((reference) => !hasTenantCondition(sql5, reference, table.columnName)).map((reference) => reference.label)
461
+ );
462
+ }
463
+ function referencesOf(sql5, table) {
464
+ const name = `"${escapeRegExp(table.name)}"`;
465
+ const qualifier = table.schema ? `"${escapeRegExp(table.schema)}"\\.${name}` : `(?<![".])${name}`;
466
+ const source = table.schema ? qualifier : `${qualifier}(?!\\.)`;
467
+ const pattern = new RegExp(`\\b(?:from|join|update)\\s+${source}(?:\\s+(?:as\\s+)?"([^"]+)")?`, "gi");
468
+ const references = /* @__PURE__ */ new Map();
469
+ for (const match of sql5.matchAll(pattern)) {
470
+ const alias = match[1];
471
+ references.set(alias ?? table.name, {
472
+ label: alias ?? table.name,
473
+ qualifier: alias ? `"${escapeRegExp(alias)}"` : qualifier
474
+ });
475
+ }
476
+ return [...references.values()];
477
+ }
478
+ function hasTenantCondition(sql5, reference, columnName) {
479
+ const condition = new RegExp(`${reference.qualifier}\\."${escapeRegExp(columnName)}"\\s*(?:=|in\\s*\\()`, "i");
480
+ return condition.test(sql5);
481
+ }
482
+ function escapeRegExp(text2) {
483
+ return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
484
+ }
400
485
 
401
486
  // src/db/manager/factory.ts
402
487
  var dbLogger2 = logger.child("@spfn/core:database");
488
+ var tenantLogger = logger.child("@spfn/core:tenant");
403
489
  function hasDatabaseConfig() {
404
490
  return env.DATABASE_URL !== void 0 || env.DATABASE_WRITE_URL !== void 0 || env.DATABASE_READ_URL !== void 0;
405
491
  }
@@ -428,6 +514,12 @@ function detectDatabasePattern() {
428
514
  }
429
515
  return { type: "none" };
430
516
  }
517
+ function createDrizzle(client) {
518
+ const queryLogger = createDevelopmentQueryLogger(env.NODE_ENV, (message, warning) => {
519
+ tenantLogger.warn(message, { tables: warning.tables, sql: warning.sql });
520
+ });
521
+ return queryLogger ? drizzle({ client, logger: queryLogger }) : drizzle({ client });
522
+ }
431
523
  async function createWriteReadClients(writeUrl, readUrl, poolConfig, retryConfig) {
432
524
  let writeClient;
433
525
  let readClient;
@@ -442,8 +534,8 @@ async function createWriteReadClients(writeUrl, readUrl, poolConfig, retryConfig
442
534
  try {
443
535
  readClient = await createDatabaseConnection(readUrl, readPoolConfig, retryConfig);
444
536
  return {
445
- write: drizzle({ client: writeClient }),
446
- read: drizzle({ client: readClient }),
537
+ write: createDrizzle(writeClient),
538
+ read: createDrizzle(readClient),
447
539
  writeClient,
448
540
  readClient
449
541
  };
@@ -459,8 +551,8 @@ async function createWriteReadClients(writeUrl, readUrl, poolConfig, retryConfig
459
551
  }
460
552
  );
461
553
  return {
462
- write: drizzle({ client: writeClient }),
463
- read: drizzle({ client: writeClient }),
554
+ write: createDrizzle(writeClient),
555
+ read: createDrizzle(writeClient),
464
556
  writeClient,
465
557
  readClient: writeClient
466
558
  };
@@ -468,7 +560,7 @@ async function createWriteReadClients(writeUrl, readUrl, poolConfig, retryConfig
468
560
  }
469
561
  async function createSingleClient(url, poolConfig, retryConfig) {
470
562
  const client = await createDatabaseConnection(url, poolConfig, retryConfig);
471
- const db = drizzle({ client });
563
+ const db = createDrizzle(client);
472
564
  return {
473
565
  write: db,
474
566
  read: db,
@@ -1252,7 +1344,7 @@ function walkDirectory(dir, maxDepth, ancestors = /* @__PURE__ */ new Set()) {
1252
1344
  if (maxDepth < 1 || !safeStat(dir)?.isDirectory()) return walk;
1253
1345
  const realDir = realpathSafe(dir);
1254
1346
  if (ancestors.has(realDir)) return walk;
1255
- const chain = new Set(ancestors).add(realDir);
1347
+ const chain2 = new Set(ancestors).add(realDir);
1256
1348
  for (const entry of safeReaddir(dir)) {
1257
1349
  const path = join(dir, entry);
1258
1350
  const stat = safeStat(path);
@@ -1261,7 +1353,7 @@ function walkDirectory(dir, maxDepth, ancestors = /* @__PURE__ */ new Set()) {
1261
1353
  } else if (stat?.isDirectory() && !SKIPPED_DIRECTORIES.test(entry)) {
1262
1354
  walk.directories.push(path);
1263
1355
  const isLink = isSymlink(path);
1264
- const child = walkDirectory(path, isLink ? Math.min(maxDepth - 1, 1) : maxDepth - 1, chain);
1356
+ const child = walkDirectory(path, isLink ? Math.min(maxDepth - 1, 1) : maxDepth - 1, chain2);
1265
1357
  for (const file of child.files) walk.files.push(file);
1266
1358
  for (const sub of child.directories) walk.directories.push(sub);
1267
1359
  }
@@ -1481,6 +1573,17 @@ function generateDrizzleConfigFile(options = {}) {
1481
1573
  function id() {
1482
1574
  return bigserial("id", { mode: "number" }).primaryKey();
1483
1575
  }
1576
+ function tenantKey(name = "tenant_id") {
1577
+ const builder = bigint(name, { mode: "number" }).notNull();
1578
+ const buildable = builder;
1579
+ const build = buildable.build;
1580
+ buildable.build = function(table) {
1581
+ const column = build.call(this, table);
1582
+ registerTenantColumn(table, column);
1583
+ return column;
1584
+ };
1585
+ return builder;
1586
+ }
1484
1587
  function timestamps() {
1485
1588
  return {
1486
1589
  createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
@@ -2168,6 +2271,32 @@ function buildWhereFromObject(table, where) {
2168
2271
  );
2169
2272
  return conditions.length === 1 ? conditions[0] : and(...conditions);
2170
2273
  }
2274
+ function buildWhereClause(table, where) {
2275
+ if (isSQLWrapper(where)) {
2276
+ return where;
2277
+ }
2278
+ return where ? buildWhereFromObject(table, where) : void 0;
2279
+ }
2280
+ async function countRows(db, table, where, options) {
2281
+ const cap = options?.cap;
2282
+ if (cap === void 0) {
2283
+ const [result2] = await db.select({ count: count$1() }).from(table).where(where);
2284
+ return Number(result2?.count ?? 0);
2285
+ }
2286
+ assertCountCap(cap);
2287
+ const capped = db.select({ one: sql`1`.as("one") }).from(table).where(where).limit(cap + 1).as("capped");
2288
+ const [result] = await db.select({ count: count$1() }).from(capped);
2289
+ return Number(result?.count ?? 0);
2290
+ }
2291
+ function assertCountCap(cap) {
2292
+ if (!Number.isSafeInteger(cap) || cap <= 0) {
2293
+ throw new RangeError(`count cap must be a positive safe integer, received ${String(cap)}`);
2294
+ }
2295
+ }
2296
+ function clampRowLimit(requested) {
2297
+ const maxRows = env.DB_MAX_ROWS;
2298
+ return maxRows > 0 ? Math.min(requested ?? maxRows, maxRows) : requested;
2299
+ }
2171
2300
 
2172
2301
  // src/db/helpers.ts
2173
2302
  async function findOne(table, where) {
@@ -2300,6 +2429,8 @@ async function count(table, where) {
2300
2429
  const [result] = await query;
2301
2430
  return Number(result?.count ?? 0);
2302
2431
  }
2432
+
2433
+ // src/db/repository-core.ts
2303
2434
  var RepositoryError = class extends Error {
2304
2435
  constructor(message, repository, method, table, originalError) {
2305
2436
  super(message);
@@ -2313,7 +2444,7 @@ var RepositoryError = class extends Error {
2313
2444
  }
2314
2445
  }
2315
2446
  };
2316
- var BaseRepository = class {
2447
+ var RepositoryCore = class {
2317
2448
  /**
2318
2449
  * Write database instance
2319
2450
  *
@@ -2405,6 +2536,10 @@ var BaseRepository = class {
2405
2536
  );
2406
2537
  }
2407
2538
  }
2539
+ };
2540
+
2541
+ // src/db/repository.ts
2542
+ var BaseRepository = class extends RepositoryCore {
2408
2543
  // ============================================================================
2409
2544
  // CRUD Methods
2410
2545
  // ============================================================================
@@ -2460,9 +2595,7 @@ var BaseRepository = class {
2460
2595
  const orderByArray = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
2461
2596
  query = query.orderBy(...orderByArray);
2462
2597
  }
2463
- const maxRows = env.DB_MAX_ROWS;
2464
- const requestedLimit = options?.limit && options.limit > 0 ? options.limit : void 0;
2465
- const effectiveLimit = maxRows > 0 ? Math.min(requestedLimit ?? maxRows, maxRows) : requestedLimit;
2598
+ const effectiveLimit = clampRowLimit(options?.limit && options.limit > 0 ? options.limit : void 0);
2466
2599
  if (effectiveLimit) {
2467
2600
  query = query.limit(effectiveLimit);
2468
2601
  }
@@ -2492,8 +2625,7 @@ var BaseRepository = class {
2492
2625
  */
2493
2626
  async _findManyKeyset(table, options) {
2494
2627
  const { cursorColumn, after, order = "asc", where } = options;
2495
- const maxRows = env.DB_MAX_ROWS;
2496
- const limit = maxRows > 0 ? Math.min(options.limit, maxRows) : options.limit;
2628
+ const limit = clampRowLimit(options.limit);
2497
2629
  const baseWhere = where ? isSQLWrapper(where) ? where : buildWhereFromObject(table, where) : void 0;
2498
2630
  const cursorPredicate = after !== void 0 ? order === "desc" ? lt(cursorColumn, after) : gt(cursorColumn, after) : void 0;
2499
2631
  const whereClause = baseWhere && cursorPredicate ? and(baseWhere, cursorPredicate) : cursorPredicate ?? baseWhere;
@@ -2666,27 +2798,270 @@ var BaseRepository = class {
2666
2798
  *
2667
2799
  * @param table - Drizzle table schema
2668
2800
  * @param where - Optional object or SQL condition
2669
- * @returns Number of records
2801
+ * @param options - `cap` stops counting past that many rows
2802
+ * @returns Number of records; with `cap`, at most `cap + 1`, which means "more than cap"
2670
2803
  *
2671
2804
  * @example
2672
2805
  * ```typescript
2673
2806
  * const total = await this._count(users);
2674
2807
  * const activeUsers = await this._count(users, { active: true });
2808
+ *
2809
+ * // A list label: 1001 reads as "1,000+"
2810
+ * const shown = await this._count(users, undefined, { cap: 1000 });
2675
2811
  * ```
2676
2812
  */
2677
- async _count(table, where) {
2678
- let query = this.readDb.select({ count: count$1() }).from(table);
2679
- if (where) {
2680
- const whereClause = isSQLWrapper(where) ? where : where ? buildWhereFromObject(table, where) : void 0;
2681
- if (whereClause) {
2682
- query = query.where(whereClause);
2813
+ async _count(table, where, options) {
2814
+ return countRows(this.readDb, table, buildWhereClause(table, where), options);
2815
+ }
2816
+ };
2817
+ function assertTenantId(tenantId) {
2818
+ if (typeof tenantId !== "number" || !Number.isSafeInteger(tenantId) || tenantId <= 0) {
2819
+ throw new TypeError(`Tenant id must be a positive safe integer, received ${String(tenantId)}`);
2820
+ }
2821
+ }
2822
+ function requireTenantColumn(table) {
2823
+ const ref = findTenantColumn(table);
2824
+ if (!ref) {
2825
+ throw new Error(
2826
+ `Table "${getTableName(table)}" has no tenantKey() column; TenantRepository methods take tenant-aware tables only`
2827
+ );
2828
+ }
2829
+ return ref;
2830
+ }
2831
+ function tenantCondition(table, tenantId) {
2832
+ return eq(requireTenantColumn(table).column, tenantId);
2833
+ }
2834
+ function optionalTenantCondition(table, tenantId) {
2835
+ const ref = findTenantColumn(table);
2836
+ return ref ? eq(ref.column, tenantId) : void 0;
2837
+ }
2838
+ function withCondition(condition, extra) {
2839
+ if (!extra) {
2840
+ return condition;
2841
+ }
2842
+ if (typeof condition === "function") {
2843
+ return (...args) => and(condition(...args), extra);
2844
+ }
2845
+ return condition ? and(condition, extra) : extra;
2846
+ }
2847
+ var REFUSED_SELECT_MEMBERS = /* @__PURE__ */ new Set([
2848
+ "rightJoin",
2849
+ "fullJoin",
2850
+ "crossJoin",
2851
+ "leftJoinLateral",
2852
+ "innerJoinLateral",
2853
+ "crossJoinLateral",
2854
+ "union",
2855
+ "unionAll",
2856
+ "intersect",
2857
+ "intersectAll",
2858
+ "except",
2859
+ "exceptAll",
2860
+ // The standalone union(a, b) / intersect / except functions from
2861
+ // drizzle-orm/pg-core call this on the left query instead of the methods above.
2862
+ "addSetOperators",
2863
+ // The factories behind the join and set operator methods. TypeScript marks
2864
+ // them private, but at runtime they are reachable and return drizzle's raw
2865
+ // closures, which would add a join or a query unscoped.
2866
+ "createJoin",
2867
+ "createSetOperator"
2868
+ ]);
2869
+ var SCOPED_JOINS = /* @__PURE__ */ new Set(["innerJoin", "leftJoin"]);
2870
+ function scopeSelectBuilder(builder, tenantId) {
2871
+ return new Proxy(builder, {
2872
+ get(target, prop, receiver) {
2873
+ if (prop !== "from") {
2874
+ return Reflect.get(target, prop, receiver);
2683
2875
  }
2876
+ return (source) => scopeSelectQuery(target.from(source), source, tenantId);
2877
+ }
2878
+ });
2879
+ }
2880
+ function scopeSelectQuery(query, fromTable, tenantId) {
2881
+ const fromCondition = tenantCondition(fromTable, tenantId);
2882
+ query.where(fromCondition);
2883
+ const proxy = new Proxy(query, {
2884
+ get: (target, prop) => scopedMember(target, prop, { proxy, fromCondition, tenantId })
2885
+ });
2886
+ return proxy;
2887
+ }
2888
+ function scopedMember(target, prop, state) {
2889
+ if (typeof prop === "string" && REFUSED_SELECT_MEMBERS.has(prop)) {
2890
+ return () => {
2891
+ throw new Error(`${prop}() is not supported on a TenantRepository _select; write this query on this.readDb`);
2892
+ };
2893
+ }
2894
+ if (prop === "where") {
2895
+ return (condition) => chain(target.where(withCondition(condition, state.fromCondition)), target, state);
2896
+ }
2897
+ if (typeof prop === "string" && SCOPED_JOINS.has(prop)) {
2898
+ return (table, on) => {
2899
+ assertNotView(table);
2900
+ return chain(target[prop](table, withCondition(on, optionalTenantCondition(table, state.tenantId))), target, state);
2901
+ };
2902
+ }
2903
+ return passThrough(target, prop, state);
2904
+ }
2905
+ function assertNotView(source) {
2906
+ if (is(source, View)) {
2907
+ throw new Error(
2908
+ "A view cannot be joined on a TenantRepository _select; join the underlying table, or write this query on this.readDb"
2909
+ );
2910
+ }
2911
+ }
2912
+ function chain(result, target, state) {
2913
+ return result === target ? state.proxy : result;
2914
+ }
2915
+ function passThrough(target, prop, state) {
2916
+ const value = Reflect.get(target, prop, target);
2917
+ if (typeof value !== "function") {
2918
+ return value;
2919
+ }
2920
+ return (...args) => chain(value.apply(target, args), target, state);
2921
+ }
2922
+
2923
+ // src/db/tenant/tenant-repository.ts
2924
+ var TenantRepository = class extends RepositoryCore {
2925
+ // ============================================================================
2926
+ // Reads
2927
+ // ============================================================================
2928
+ /** One row of the tenant matching `where`, or null. */
2929
+ async _findOne(tenantId, table, where) {
2930
+ const condition = this.requiredWhere(tenantId, table, where, "findOne");
2931
+ const rows = await this.readDb.select().from(table).where(condition).limit(1);
2932
+ return rows[0] ?? null;
2933
+ }
2934
+ /** Rows of the tenant, with the same options and DB_MAX_ROWS ceiling as BaseRepository. */
2935
+ async _findMany(tenantId, table, options) {
2936
+ let query = this.readDb.select().from(table).where(this.tenantWhere(tenantId, table, options?.where));
2937
+ if (options?.orderBy) {
2938
+ query = query.orderBy(...Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy]);
2939
+ }
2940
+ const limit = clampRowLimit(options?.limit && options.limit > 0 ? options.limit : void 0);
2941
+ if (limit) {
2942
+ query = query.limit(limit);
2943
+ }
2944
+ if (options?.offset) {
2945
+ query = query.offset(options.offset);
2946
+ }
2947
+ return query;
2948
+ }
2949
+ /**
2950
+ * Keyset (cursor) page of the tenant's rows. Index the table on
2951
+ * (tenant column, cursor column) so a page reads only its own rows.
2952
+ */
2953
+ async _findManyKeyset(tenantId, table, options) {
2954
+ const { cursorColumn, after, order = "asc" } = options;
2955
+ const cursor = after === void 0 ? void 0 : order === "desc" ? lt(cursorColumn, after) : gt(cursorColumn, after);
2956
+ const query = this.readDb.select().from(table).where(and(this.tenantWhere(tenantId, table, options.where), cursor)).orderBy(order === "desc" ? desc(cursorColumn) : asc(cursorColumn)).limit(clampRowLimit(options.limit));
2957
+ return query;
2958
+ }
2959
+ /** Number of the tenant's rows; with `cap`, at most `cap + 1` ("more than cap"). */
2960
+ async _count(tenantId, table, where, options) {
2961
+ const condition = this.tenantWhere(tenantId, table, where);
2962
+ return countRows(this.readDb, table, condition, options);
2963
+ }
2964
+ _select(tenantId, fields) {
2965
+ assertTenantId(tenantId);
2966
+ return scopeSelectBuilder(fields ? this.readDb.select(fields) : this.readDb.select(), tenantId);
2967
+ }
2968
+ // ============================================================================
2969
+ // Writes
2970
+ // ============================================================================
2971
+ /** Insert one row for the tenant; a different tenant value in `data` throws. */
2972
+ async _create(tenantId, table, data) {
2973
+ const values = this.withTenantValue(tenantId, table, data);
2974
+ const rows = await this.db.insert(table).values(values).returning();
2975
+ return rows[0];
2976
+ }
2977
+ /** Insert rows for the tenant; every row is checked before any is written. */
2978
+ async _createMany(tenantId, table, data) {
2979
+ const values = data.map((row) => this.withTenantValue(tenantId, table, row));
2980
+ const rows = await this.db.insert(table).values(values).returning();
2981
+ return rows;
2982
+ }
2983
+ /**
2984
+ * Insert, or update on conflict. The conflict target must include the
2985
+ * tenant column: on `id` alone, an insert could collide with another
2986
+ * tenant's row and update it.
2987
+ */
2988
+ async _upsert(tenantId, table, data, options) {
2989
+ const values = this.withTenantValue(tenantId, table, data);
2990
+ this.assertTargetHasTenant(table, options.target);
2991
+ this.assertTenantUnchanged(table, options.set);
2992
+ const rows = await this.db.insert(table).values(values).onConflictDoUpdate({ target: options.target, set: options.set || values }).returning();
2993
+ return rows[0];
2994
+ }
2995
+ /** Update the tenant's rows matching `where`; returns the first, or null. */
2996
+ async _updateOne(tenantId, table, where, data) {
2997
+ const rows = await this.updateRows(tenantId, table, where, data, "updateOne");
2998
+ return rows[0] ?? null;
2999
+ }
3000
+ /** Update the tenant's rows matching `where`. */
3001
+ async _updateMany(tenantId, table, where, data) {
3002
+ return this.updateRows(tenantId, table, where, data, "updateMany");
3003
+ }
3004
+ /** Delete the tenant's rows matching `where`; returns the first, or null. */
3005
+ async _deleteOne(tenantId, table, where) {
3006
+ const rows = await this.deleteRows(tenantId, table, where, "deleteOne");
3007
+ return rows[0] ?? null;
3008
+ }
3009
+ /** Delete the tenant's rows matching `where`. */
3010
+ async _deleteMany(tenantId, table, where) {
3011
+ return this.deleteRows(tenantId, table, where, "deleteMany");
3012
+ }
3013
+ // ============================================================================
3014
+ // Internals — private, so a subclass cannot reach an unscoped path through them
3015
+ // ============================================================================
3016
+ async updateRows(tenantId, table, where, data, method) {
3017
+ const condition = this.requiredWhere(tenantId, table, where, method);
3018
+ this.assertTenantUnchanged(table, data);
3019
+ const rows = await this.db.update(table).set(data).where(condition).returning();
3020
+ return rows;
3021
+ }
3022
+ async deleteRows(tenantId, table, where, method) {
3023
+ const condition = this.requiredWhere(tenantId, table, where, method);
3024
+ const rows = await this.db.delete(table).where(condition).returning();
3025
+ return rows;
3026
+ }
3027
+ /** `tenant condition AND where`, after checking the tenant id and the table. */
3028
+ tenantWhere(tenantId, table, where) {
3029
+ assertTenantId(tenantId);
3030
+ return and(tenantCondition(table, tenantId), buildWhereClause(table, where));
3031
+ }
3032
+ /** As tenantWhere, and `where` itself must filter on something besides the tenant. */
3033
+ requiredWhere(tenantId, table, where, method) {
3034
+ assertTenantId(tenantId);
3035
+ const tenant = tenantCondition(table, tenantId);
3036
+ const filter = buildWhereClause(table, where);
3037
+ if (!filter) {
3038
+ throw new Error(`${method} requires at least one where condition`);
3039
+ }
3040
+ return and(tenant, filter);
3041
+ }
3042
+ withTenantValue(tenantId, table, data) {
3043
+ assertTenantId(tenantId);
3044
+ const { key } = requireTenantColumn(table);
3045
+ const given = data[key];
3046
+ if (given !== void 0 && given !== tenantId) {
3047
+ throw new Error(`${key} ${String(given)} does not match the tenant ${tenantId}`);
3048
+ }
3049
+ return { ...data, [key]: tenantId };
3050
+ }
3051
+ assertTenantUnchanged(table, data) {
3052
+ const { key } = requireTenantColumn(table);
3053
+ if (data && data[key] !== void 0) {
3054
+ throw new Error(`${key} cannot be changed through TenantRepository`);
3055
+ }
3056
+ }
3057
+ assertTargetHasTenant(table, target) {
3058
+ const { column } = requireTenantColumn(table);
3059
+ if (!target.some((targetColumn) => targetColumn.name === column.name)) {
3060
+ throw new Error(`upsert target must include the tenant column "${column.name}"`);
2684
3061
  }
2685
- const [result] = await query;
2686
- return Number(result?.count ?? 0);
2687
3062
  }
2688
3063
  };
2689
3064
 
2690
- export { BaseRepository, PROJECT_MIGRATIONS_TABLE, PROJECT_TARGET_NAME, RUN_MIGRATIONS_HINT, RepositoryError, Transactional, auditFields, checkConnection, closeDatabase, collectMigrationStatus, count, countPendingMigrations, create, createDatabaseConnection, createDatabaseFromEnv, createMany, createSchema, deleteMany, deleteOne, detectDialect, discoverFunctionMigrations, enumText, filterPendingEntries, findMany, findOne, forceReconnectDatabase, foreignKey, formatPendingMigrations, fromPostgresError, functionMigrationsTable, generateDrizzleConfigFile, getDatabase, getDatabaseInfo, getDrizzleConfig, getSchemaInfo, getTransaction, getTransactionContext, hasMigrationTargets, id, initDatabase, isConnectionLevelError, migrationTargets, onAfterCommit, onAfterRollback, onBeforeCommit, optionalForeignKey, packageNameToSchema, pendingMigrationTargets, pendingMigrationsSummary, projectMigrationsDir, publishingFields, readMigrationEntries, renderDrizzleConfig, reportDatabaseError, resetConnectionErrorCounter, runInTransaction, runWithTransaction, setDatabase, setDatabaseProvider, softDelete, timestamps, typedJsonb, updateMany, updateOne, upsert, utcTimestamp, uuid, verificationTimestamp };
3065
+ export { BaseRepository, PROJECT_MIGRATIONS_TABLE, PROJECT_TARGET_NAME, RUN_MIGRATIONS_HINT, RepositoryError, TenantRepository, Transactional, auditFields, checkConnection, closeDatabase, collectMigrationStatus, count, countPendingMigrations, create, createDatabaseConnection, createDatabaseFromEnv, createMany, createSchema, deleteMany, deleteOne, detectDialect, discoverFunctionMigrations, enumText, filterPendingEntries, findMany, findOne, forceReconnectDatabase, foreignKey, formatPendingMigrations, fromPostgresError, functionMigrationsTable, generateDrizzleConfigFile, getDatabase, getDatabaseInfo, getDrizzleConfig, getSchemaInfo, getTransaction, getTransactionContext, hasMigrationTargets, id, initDatabase, isConnectionLevelError, migrationTargets, onAfterCommit, onAfterRollback, onBeforeCommit, optionalForeignKey, packageNameToSchema, pendingMigrationTargets, pendingMigrationsSummary, projectMigrationsDir, publishingFields, readMigrationEntries, renderDrizzleConfig, reportDatabaseError, resetConnectionErrorCounter, runInTransaction, runWithTransaction, setDatabase, setDatabaseProvider, softDelete, tenantKey, timestamps, typedJsonb, updateMany, updateOne, upsert, utcTimestamp, uuid, verificationTimestamp };
2691
3066
  //# sourceMappingURL=index.js.map
2692
3067
  //# sourceMappingURL=index.js.map