@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/README.md +1 -1
- package/dist/db/index.d.ts +254 -84
- package/dist/db/index.js +400 -25
- package/dist/db/index.js.map +1 -1
- package/dist/event/index.d.ts +2 -2
- package/dist/event/index.js +32 -4
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +2 -2
- package/dist/event/sse/client.js +12 -4
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +40 -5
- package/dist/event/sse/index.js +437 -28
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +2 -2
- package/dist/event/ws/index.d.ts +3 -3
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +223 -37
- package/dist/server/index.js.map +1 -1
- package/dist/state/client.d.ts +8 -1
- package/dist/state/client.js +15 -5
- package/dist/state/client.js.map +1 -1
- package/dist/state/react.d.ts +3 -1
- package/dist/state/react.js.map +1 -1
- package/dist/{token-manager-Ckjt2-qq.d.ts → token-manager-CvOsAbnK.d.ts} +47 -3
- package/dist/{types-CurMab2Q.d.ts → types-BrO7j6mR.d.ts} +17 -3
- package/dist/{types-DncqXfWN.d.ts → types-DH6d2eOb.d.ts} +1 -1
- package/package.json +1 -1
- package/src/db/README.md +109 -6
- package/src/db/schema/README.md +19 -1
- package/src/event/README.md +66 -6
- package/src/state/README.md +9 -1
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 {
|
|
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:
|
|
446
|
-
read:
|
|
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:
|
|
463
|
-
read:
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
* @
|
|
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
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
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
|