@c15t/backend 2.0.0-rc.1 → 2.0.0-rc.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/README.md +3 -3
- package/dist/302.js +473 -0
- package/dist/583.js +540 -0
- package/dist/915.js +1771 -0
- package/dist/cache.cjs +5 -5
- package/dist/cache.js +4 -415
- package/dist/core.cjs +1356 -120
- package/dist/core.js +163 -1981
- package/dist/db/adapters/drizzle.cjs +1 -1
- package/dist/db/adapters/drizzle.js +1 -2
- package/dist/db/adapters/kysely.cjs +1 -1
- package/dist/db/adapters/kysely.js +1 -2
- package/dist/db/adapters/mongo.cjs +1 -1
- package/dist/db/adapters/mongo.js +1 -2
- package/dist/db/adapters/prisma.cjs +1 -1
- package/dist/db/adapters/prisma.js +1 -2
- package/dist/db/adapters/typeorm.cjs +1 -1
- package/dist/db/adapters/typeorm.js +1 -2
- package/dist/db/adapters.cjs +1 -1
- package/dist/db/migrator.cjs +1 -1
- package/dist/db/schema.cjs +43 -3
- package/dist/db/schema.js +35 -4
- package/dist/define-config.cjs +1 -1
- package/dist/edge.cjs +1106 -0
- package/dist/edge.js +190 -0
- package/dist/router.cjs +885 -123
- package/dist/router.js +1 -1507
- package/dist/{types.cjs → types/index.cjs} +1 -1
- package/{dist → dist-types}/cache/adapters/cloudflare-kv.d.ts +0 -1
- package/{dist → dist-types}/cache/adapters/index.d.ts +0 -1
- package/{dist → dist-types}/cache/adapters/memory.d.ts +0 -1
- package/{dist → dist-types}/cache/adapters/upstash-redis.d.ts +0 -1
- package/{dist → dist-types}/cache/gvl-resolver.d.ts +0 -1
- package/{dist → dist-types}/cache/index.d.ts +0 -1
- package/{dist → dist-types}/cache/keys.d.ts +0 -1
- package/{dist → dist-types}/cache/types.d.ts +0 -1
- package/{dist → dist-types}/core.d.ts +8 -1
- package/{dist → dist-types}/db/migrator/index.d.ts +0 -1
- package/dist-types/db/registry/consent-policy.d.ts +78 -0
- package/{dist → dist-types}/db/registry/consent-purpose.d.ts +0 -1
- package/{dist → dist-types}/db/registry/domain.d.ts +0 -1
- package/dist-types/db/registry/index.d.ts +118 -0
- package/dist-types/db/registry/runtime-policy-decision.d.ts +60 -0
- package/{dist → dist-types}/db/registry/subject.d.ts +0 -2
- package/{dist → dist-types}/db/registry/types.d.ts +1 -1
- package/{dist → dist-types}/db/registry/utils/generate-id.d.ts +0 -1
- package/{dist → dist-types}/db/registry/utils.d.ts +0 -1
- package/{dist → dist-types}/db/schema/1.0.0/audit-log.d.ts +0 -1
- package/{dist → dist-types}/db/schema/1.0.0/consent-policy.d.ts +0 -1
- package/{dist → dist-types}/db/schema/1.0.0/consent-purpose.d.ts +0 -1
- package/{dist → dist-types}/db/schema/1.0.0/consent-record.d.ts +0 -1
- package/{dist → dist-types}/db/schema/1.0.0/consent.d.ts +1 -2
- package/{dist → dist-types}/db/schema/1.0.0/domain.d.ts +0 -1
- package/{dist → dist-types}/db/schema/1.0.0/index.d.ts +0 -32
- package/{dist → dist-types}/db/schema/1.0.0/subject.d.ts +0 -2
- package/{dist → dist-types}/db/schema/2.0.0/audit-log.d.ts +1 -2
- package/{dist → dist-types}/db/schema/2.0.0/consent-policy.d.ts +3 -3
- package/{dist → dist-types}/db/schema/2.0.0/consent-purpose.d.ts +1 -2
- package/{dist → dist-types}/db/schema/2.0.0/consent.d.ts +7 -2
- package/{dist → dist-types}/db/schema/2.0.0/domain.d.ts +1 -2
- package/{dist → dist-types}/db/schema/2.0.0/index.d.ts +455 -28
- package/dist-types/db/schema/2.0.0/runtime-policy-decision.d.ts +23 -0
- package/{dist → dist-types}/db/schema/2.0.0/subject.d.ts +1 -3
- package/{dist → dist-types}/db/schema/index.d.ts +908 -86
- package/{dist → dist-types}/db/tenant-scope.d.ts +0 -1
- package/dist-types/define-config.d.ts +17 -0
- package/dist-types/edge/index.d.ts +5 -0
- package/dist-types/edge/init-handler.d.ts +40 -0
- package/dist-types/edge/resolve-consent.d.ts +80 -0
- package/dist-types/edge/types.d.ts +13 -0
- package/{dist → dist-types}/handlers/consent/check.handler.d.ts +0 -1
- package/{src/handlers/consent/index.ts → dist-types/handlers/consent/index.d.ts} +0 -1
- package/{dist → dist-types}/handlers/init/geo.d.ts +2 -3
- package/{dist → dist-types}/handlers/init/index.d.ts +2 -3
- package/dist-types/handlers/init/policy.d.ts +26 -0
- package/dist-types/handlers/init/resolve-init.d.ts +44 -0
- package/dist-types/handlers/init/translations.d.ts +48 -0
- package/dist-types/handlers/legal-document/current.handler.d.ts +11 -0
- package/dist-types/handlers/legal-document/snapshot.d.ts +39 -0
- package/dist-types/handlers/policy/snapshot.d.ts +99 -0
- package/{src/handlers/status/index.ts → dist-types/handlers/status/index.d.ts} +0 -1
- package/{dist → dist-types}/handlers/status/status.handler.d.ts +0 -1
- package/{dist → dist-types}/handlers/subject/get.handler.d.ts +3 -2
- package/{src/handlers/subject/index.ts → dist-types/handlers/subject/index.d.ts} +0 -1
- package/{dist → dist-types}/handlers/subject/list.handler.d.ts +3 -2
- package/{dist → dist-types}/handlers/subject/patch.handler.d.ts +0 -2
- package/{dist → dist-types}/handlers/subject/post.handler.d.ts +12 -1
- package/{dist → dist-types}/handlers/utils/consent-enrichment.d.ts +3 -1
- package/{dist → dist-types}/init.d.ts +4 -7
- package/{dist → dist-types}/middleware/auth/index.d.ts +0 -1
- package/{dist → dist-types}/middleware/auth/validate-api-key.d.ts +0 -1
- package/{dist → dist-types}/middleware/cors/cors.d.ts +0 -1
- package/{src/middleware/cors/index.ts → dist-types/middleware/cors/index.d.ts} +0 -1
- package/{dist → dist-types}/middleware/cors/is-origin-trusted.d.ts +0 -1
- package/{dist → dist-types}/middleware/cors/process-cors.d.ts +0 -1
- package/{dist → dist-types}/middleware/openapi/config.d.ts +0 -1
- package/{dist → dist-types}/middleware/openapi/handlers.d.ts +0 -1
- package/{src/middleware/openapi/index.ts → dist-types/middleware/openapi/index.d.ts} +0 -1
- package/{dist → dist-types}/middleware/process-ip/index.d.ts +0 -1
- package/dist-types/policies/builder.d.ts +127 -0
- package/dist-types/policies/defaults.d.ts +2 -0
- package/dist-types/policies/matchers.d.ts +3 -0
- package/{dist → dist-types}/router.d.ts +0 -1
- package/{dist → dist-types}/routes/consent.d.ts +0 -1
- package/{dist → dist-types}/routes/index.d.ts +1 -1
- package/{dist → dist-types}/routes/init.d.ts +0 -1
- package/dist-types/routes/legal-document.d.ts +7 -0
- package/{dist → dist-types}/routes/status.d.ts +0 -1
- package/{dist → dist-types}/routes/subject.d.ts +0 -1
- package/{dist → dist-types}/types/api.d.ts +0 -1
- package/dist-types/types/index.d.ts +464 -0
- package/dist-types/utils/background.d.ts +6 -0
- package/{dist → dist-types}/utils/create-telemetry-options.d.ts +1 -2
- package/{dist → dist-types}/utils/env.d.ts +0 -1
- package/{dist → dist-types}/utils/extract-error-message.d.ts +0 -1
- package/{dist → dist-types}/utils/instrumentation.d.ts +2 -3
- package/{dist → dist-types}/utils/logger.d.ts +0 -1
- package/{dist → dist-types}/utils/metrics.d.ts +0 -1
- package/dist-types/version.d.ts +1 -0
- package/docs/README.md +49 -0
- package/docs/api/configuration.md +208 -0
- package/docs/api/endpoints.md +211 -0
- package/docs/guides/caching.md +85 -0
- package/docs/guides/database-setup.md +128 -0
- package/docs/guides/edge-deployment.md +251 -0
- package/docs/guides/framework-integration.md +142 -0
- package/docs/guides/iab-tcf.md +89 -0
- package/docs/guides/observability.md +96 -0
- package/docs/guides/policy-packs.md +396 -0
- package/docs/quickstart.md +129 -0
- package/package.json +51 -37
- package/.turbo/turbo-build.log +0 -49
- package/CHANGELOG.md +0 -99
- package/dist/cache/adapters/cloudflare-kv.d.ts.map +0 -1
- package/dist/cache/adapters/index.d.ts.map +0 -1
- package/dist/cache/adapters/memory.d.ts.map +0 -1
- package/dist/cache/adapters/upstash-redis.d.ts.map +0 -1
- package/dist/cache/gvl-resolver.d.ts.map +0 -1
- package/dist/cache/index.d.ts.map +0 -1
- package/dist/cache/keys.d.ts.map +0 -1
- package/dist/cache/types.d.ts.map +0 -1
- package/dist/core.d.ts.map +0 -1
- package/dist/db/adapters/drizzle.d.ts +0 -2
- package/dist/db/adapters/drizzle.d.ts.map +0 -1
- package/dist/db/adapters/index.d.ts +0 -2
- package/dist/db/adapters/index.d.ts.map +0 -1
- package/dist/db/adapters/kysely.d.ts +0 -2
- package/dist/db/adapters/kysely.d.ts.map +0 -1
- package/dist/db/adapters/mongo.d.ts +0 -2
- package/dist/db/adapters/mongo.d.ts.map +0 -1
- package/dist/db/adapters/prisma.d.ts +0 -2
- package/dist/db/adapters/prisma.d.ts.map +0 -1
- package/dist/db/adapters/typeorm.d.ts +0 -2
- package/dist/db/adapters/typeorm.d.ts.map +0 -1
- package/dist/db/migrator/index.d.ts.map +0 -1
- package/dist/db/registry/consent-policy.d.ts +0 -23
- package/dist/db/registry/consent-policy.d.ts.map +0 -1
- package/dist/db/registry/consent-purpose.d.ts.map +0 -1
- package/dist/db/registry/domain.d.ts.map +0 -1
- package/dist/db/registry/index.d.ts +0 -57
- package/dist/db/registry/index.d.ts.map +0 -1
- package/dist/db/registry/subject.d.ts.map +0 -1
- package/dist/db/registry/types.d.ts.map +0 -1
- package/dist/db/registry/utils/generate-id.d.ts.map +0 -1
- package/dist/db/registry/utils.d.ts.map +0 -1
- package/dist/db/schema/1.0.0/audit-log.d.ts.map +0 -1
- package/dist/db/schema/1.0.0/consent-policy.d.ts.map +0 -1
- package/dist/db/schema/1.0.0/consent-purpose.d.ts.map +0 -1
- package/dist/db/schema/1.0.0/consent-record.d.ts.map +0 -1
- package/dist/db/schema/1.0.0/consent.d.ts.map +0 -1
- package/dist/db/schema/1.0.0/domain.d.ts.map +0 -1
- package/dist/db/schema/1.0.0/index.d.ts.map +0 -1
- package/dist/db/schema/1.0.0/subject.d.ts.map +0 -1
- package/dist/db/schema/2.0.0/audit-log.d.ts.map +0 -1
- package/dist/db/schema/2.0.0/consent-policy.d.ts.map +0 -1
- package/dist/db/schema/2.0.0/consent-purpose.d.ts.map +0 -1
- package/dist/db/schema/2.0.0/consent.d.ts.map +0 -1
- package/dist/db/schema/2.0.0/domain.d.ts.map +0 -1
- package/dist/db/schema/2.0.0/index.d.ts.map +0 -1
- package/dist/db/schema/2.0.0/subject.d.ts.map +0 -1
- package/dist/db/schema/index.d.ts.map +0 -1
- package/dist/db/tenant-scope.d.ts.map +0 -1
- package/dist/define-config.d.ts +0 -5
- package/dist/define-config.d.ts.map +0 -1
- package/dist/handlers/consent/check.handler.d.ts.map +0 -1
- package/dist/handlers/consent/index.d.ts +0 -12
- package/dist/handlers/consent/index.d.ts.map +0 -1
- package/dist/handlers/init/geo.d.ts.map +0 -1
- package/dist/handlers/init/index.d.ts.map +0 -1
- package/dist/handlers/init/translations.d.ts +0 -28
- package/dist/handlers/init/translations.d.ts.map +0 -1
- package/dist/handlers/status/index.d.ts +0 -7
- package/dist/handlers/status/index.d.ts.map +0 -1
- package/dist/handlers/status/status.handler.d.ts.map +0 -1
- package/dist/handlers/subject/get.handler.d.ts.map +0 -1
- package/dist/handlers/subject/index.d.ts +0 -10
- package/dist/handlers/subject/index.d.ts.map +0 -1
- package/dist/handlers/subject/list.handler.d.ts.map +0 -1
- package/dist/handlers/subject/patch.handler.d.ts.map +0 -1
- package/dist/handlers/subject/post.handler.d.ts.map +0 -1
- package/dist/handlers/utils/consent-enrichment.d.ts.map +0 -1
- package/dist/init.d.ts.map +0 -1
- package/dist/middleware/auth/index.d.ts.map +0 -1
- package/dist/middleware/auth/validate-api-key.d.ts.map +0 -1
- package/dist/middleware/cors/cors.d.ts.map +0 -1
- package/dist/middleware/cors/index.d.ts +0 -30
- package/dist/middleware/cors/index.d.ts.map +0 -1
- package/dist/middleware/cors/is-origin-trusted.d.ts.map +0 -1
- package/dist/middleware/cors/process-cors.d.ts.map +0 -1
- package/dist/middleware/openapi/config.d.ts.map +0 -1
- package/dist/middleware/openapi/handlers.d.ts.map +0 -1
- package/dist/middleware/openapi/index.d.ts +0 -12
- package/dist/middleware/openapi/index.d.ts.map +0 -1
- package/dist/middleware/process-ip/index.d.ts.map +0 -1
- package/dist/router.d.ts.map +0 -1
- package/dist/routes/consent.d.ts.map +0 -1
- package/dist/routes/index.d.ts.map +0 -1
- package/dist/routes/init.d.ts.map +0 -1
- package/dist/routes/status.d.ts.map +0 -1
- package/dist/routes/subject.d.ts.map +0 -1
- package/dist/types/api.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -255
- package/dist/types/index.d.ts.map +0 -1
- package/dist/utils/create-telemetry-options.d.ts.map +0 -1
- package/dist/utils/env.d.ts.map +0 -1
- package/dist/utils/extract-error-message.d.ts.map +0 -1
- package/dist/utils/index.d.ts +0 -4
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/instrumentation.d.ts.map +0 -1
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/metrics.d.ts.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
- package/knip.json +0 -31
- package/rslib.config.ts +0 -93
- package/src/cache/adapters/cloudflare-kv.ts +0 -71
- package/src/cache/adapters/index.ts +0 -22
- package/src/cache/adapters/memory.ts +0 -111
- package/src/cache/adapters/upstash-redis.ts +0 -113
- package/src/cache/gvl-resolver.ts +0 -289
- package/src/cache/index.ts +0 -34
- package/src/cache/keys.ts +0 -68
- package/src/cache/types.ts +0 -66
- package/src/core.ts +0 -368
- package/src/db/migrator/index.ts +0 -80
- package/src/db/registry/consent-policy.test.ts +0 -451
- package/src/db/registry/consent-policy.ts +0 -82
- package/src/db/registry/consent-purpose.test.ts +0 -428
- package/src/db/registry/consent-purpose.ts +0 -61
- package/src/db/registry/domain.test.ts +0 -445
- package/src/db/registry/domain.ts +0 -91
- package/src/db/registry/index.ts +0 -14
- package/src/db/registry/subject.test.ts +0 -388
- package/src/db/registry/subject.ts +0 -129
- package/src/db/registry/types.ts +0 -10
- package/src/db/registry/utils/generate-id.test.ts +0 -216
- package/src/db/registry/utils/generate-id.ts +0 -133
- package/src/db/registry/utils.ts +0 -133
- package/src/db/schema/1.0.0/audit-log.ts +0 -15
- package/src/db/schema/1.0.0/consent-policy.ts +0 -14
- package/src/db/schema/1.0.0/consent-purpose.ts +0 -14
- package/src/db/schema/1.0.0/consent-record.ts +0 -10
- package/src/db/schema/1.0.0/consent.ts +0 -20
- package/src/db/schema/1.0.0/domain.ts +0 -12
- package/src/db/schema/1.0.0/index.ts +0 -48
- package/src/db/schema/1.0.0/subject.ts +0 -12
- package/src/db/schema/2.0.0/audit-log.ts +0 -18
- package/src/db/schema/2.0.0/consent-policy.ts +0 -28
- package/src/db/schema/2.0.0/consent-purpose.ts +0 -12
- package/src/db/schema/2.0.0/consent.ts +0 -26
- package/src/db/schema/2.0.0/domain.ts +0 -12
- package/src/db/schema/2.0.0/index.ts +0 -47
- package/src/db/schema/2.0.0/subject.ts +0 -14
- package/src/db/schema/index.ts +0 -15
- package/src/db/tenant-scope.test.ts +0 -750
- package/src/db/tenant-scope.ts +0 -103
- package/src/define-config.ts +0 -5
- package/src/handlers/consent/check.handler.ts +0 -126
- package/src/handlers/init/geo.test.ts +0 -317
- package/src/handlers/init/geo.ts +0 -195
- package/src/handlers/init/index.test.ts +0 -205
- package/src/handlers/init/index.ts +0 -114
- package/src/handlers/init/translations.test.ts +0 -121
- package/src/handlers/init/translations.ts +0 -72
- package/src/handlers/status/status.handler.test.ts +0 -155
- package/src/handlers/status/status.handler.ts +0 -51
- package/src/handlers/subject/get.handler.ts +0 -93
- package/src/handlers/subject/list.handler.ts +0 -93
- package/src/handlers/subject/patch.handler.ts +0 -122
- package/src/handlers/subject/post.handler.test.ts +0 -294
- package/src/handlers/subject/post.handler.ts +0 -254
- package/src/handlers/utils/consent-enrichment.test.ts +0 -380
- package/src/handlers/utils/consent-enrichment.ts +0 -218
- package/src/init.test.ts +0 -126
- package/src/init.ts +0 -87
- package/src/middleware/auth/index.ts +0 -11
- package/src/middleware/auth/validate-api-key.test.ts +0 -86
- package/src/middleware/auth/validate-api-key.ts +0 -107
- package/src/middleware/cors/cors.test.ts +0 -135
- package/src/middleware/cors/cors.ts +0 -186
- package/src/middleware/cors/is-origin-trusted.test.ts +0 -164
- package/src/middleware/cors/is-origin-trusted.ts +0 -130
- package/src/middleware/cors/process-cors.ts +0 -91
- package/src/middleware/openapi/config.ts +0 -29
- package/src/middleware/openapi/handlers.ts +0 -34
- package/src/middleware/process-ip/index.test.ts +0 -195
- package/src/middleware/process-ip/index.ts +0 -199
- package/src/router.ts +0 -15
- package/src/routes/consent.ts +0 -52
- package/src/routes/index.ts +0 -10
- package/src/routes/init.ts +0 -102
- package/src/routes/status.ts +0 -46
- package/src/routes/subject.ts +0 -152
- package/src/types/api.ts +0 -48
- package/src/types/index.ts +0 -288
- package/src/utils/create-telemetry-options.test.ts +0 -302
- package/src/utils/create-telemetry-options.ts +0 -229
- package/src/utils/env.ts +0 -84
- package/src/utils/extract-error-message.ts +0 -21
- package/src/utils/instrumentation.test.ts +0 -185
- package/src/utils/instrumentation.ts +0 -196
- package/src/utils/logger.ts +0 -41
- package/src/utils/metrics.test.ts +0 -323
- package/src/utils/metrics.ts +0 -402
- package/src/utils/telemetry-pii.test.ts +0 -325
- package/src/version.ts +0 -2
- package/tsconfig.json +0 -11
- package/vitest.config.ts +0 -28
- /package/dist/{types.js → types/index.js} +0 -0
- /package/{src/db/adapters/drizzle.ts → dist-types/db/adapters/drizzle.d.ts} +0 -0
- /package/{src/db/adapters/index.ts → dist-types/db/adapters/index.d.ts} +0 -0
- /package/{src/db/adapters/kysely.ts → dist-types/db/adapters/kysely.d.ts} +0 -0
- /package/{src/db/adapters/mongo.ts → dist-types/db/adapters/mongo.d.ts} +0 -0
- /package/{src/db/adapters/prisma.ts → dist-types/db/adapters/prisma.d.ts} +0 -0
- /package/{src/db/adapters/typeorm.ts → dist-types/db/adapters/typeorm.d.ts} +0 -0
- /package/{src/utils/index.ts → dist-types/utils/index.d.ts} +0 -0
|
@@ -1,750 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { withTenantScope } from './tenant-scope';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* A minimal in-memory database that implements the ORM interface well enough
|
|
6
|
-
* to prove real data isolation. Records are stored in a flat Map<table, rows[]>
|
|
7
|
-
* and the where-builder evaluates conditions against actual row data.
|
|
8
|
-
*/
|
|
9
|
-
function createInMemoryOrm() {
|
|
10
|
-
const store = new Map<string, Record<string, any>[]>();
|
|
11
|
-
|
|
12
|
-
const getTable = (table: string) => {
|
|
13
|
-
if (!store.has(table)) store.set(table, []);
|
|
14
|
-
return store.get(table)!;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// Minimal where-builder that evaluates conditions against a row
|
|
18
|
-
const createBuilder = (row: Record<string, any>) => {
|
|
19
|
-
const b: any = (col: string, op: string, val: any) => {
|
|
20
|
-
if (op === '=') return row[col] === val;
|
|
21
|
-
if (op === '!=') return row[col] !== val;
|
|
22
|
-
return false;
|
|
23
|
-
};
|
|
24
|
-
b.and = (...conds: boolean[]) => conds.every(Boolean);
|
|
25
|
-
b.or = (...conds: boolean[]) => conds.some(Boolean);
|
|
26
|
-
return b;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const matchesWhere = (
|
|
30
|
-
row: Record<string, any>,
|
|
31
|
-
where?: (b: any) => boolean
|
|
32
|
-
) => {
|
|
33
|
-
if (!where) return true;
|
|
34
|
-
return where(createBuilder(row));
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const orm: any = {
|
|
38
|
-
create: (table: string, data: any) => {
|
|
39
|
-
getTable(table).push({ ...data });
|
|
40
|
-
return Promise.resolve({ ...data });
|
|
41
|
-
},
|
|
42
|
-
|
|
43
|
-
createMany: (table: string, items: any[]) => {
|
|
44
|
-
const rows = items.map((d) => ({ ...d }));
|
|
45
|
-
getTable(table).push(...rows);
|
|
46
|
-
return Promise.resolve(rows);
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
findFirst: (table: string, opts?: any) => {
|
|
50
|
-
const rows = getTable(table);
|
|
51
|
-
const found = rows.find((r) => matchesWhere(r, opts?.where));
|
|
52
|
-
return Promise.resolve(found ?? null);
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
findMany: (table: string, opts?: any) => {
|
|
56
|
-
const rows = getTable(table);
|
|
57
|
-
return Promise.resolve(rows.filter((r) => matchesWhere(r, opts?.where)));
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
count: (table: string, opts?: any) => {
|
|
61
|
-
const rows = getTable(table);
|
|
62
|
-
return Promise.resolve(
|
|
63
|
-
rows.filter((r) => matchesWhere(r, opts?.where)).length
|
|
64
|
-
);
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
updateMany: (table: string, opts: any) => {
|
|
68
|
-
const rows = getTable(table);
|
|
69
|
-
let updated = 0;
|
|
70
|
-
for (const row of rows) {
|
|
71
|
-
if (matchesWhere(row, opts?.where)) {
|
|
72
|
-
Object.assign(row, opts.set);
|
|
73
|
-
updated++;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return Promise.resolve(updated);
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
deleteMany: (table: string, opts: any) => {
|
|
80
|
-
const rows = getTable(table);
|
|
81
|
-
const remaining = rows.filter((r) => !matchesWhere(r, opts?.where));
|
|
82
|
-
const deleted = rows.length - remaining.length;
|
|
83
|
-
store.set(table, remaining);
|
|
84
|
-
return Promise.resolve(deleted);
|
|
85
|
-
},
|
|
86
|
-
|
|
87
|
-
upsert: (table: string, opts: any) => {
|
|
88
|
-
const rows = getTable(table);
|
|
89
|
-
const existing = rows.find((r) => matchesWhere(r, opts?.where));
|
|
90
|
-
if (existing) {
|
|
91
|
-
Object.assign(existing, opts.update);
|
|
92
|
-
return Promise.resolve(existing);
|
|
93
|
-
}
|
|
94
|
-
const created = { ...opts.create };
|
|
95
|
-
rows.push(created);
|
|
96
|
-
return Promise.resolve(created);
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
transaction: (fn: any) => fn(orm),
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
return { orm, store };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function createMockOrm() {
|
|
106
|
-
return {
|
|
107
|
-
create: vi.fn().mockResolvedValue({ id: 'test_1' }),
|
|
108
|
-
createMany: vi.fn().mockResolvedValue([{ _id: 'test_1' }]),
|
|
109
|
-
findFirst: vi.fn().mockResolvedValue({ id: 'test_1' }),
|
|
110
|
-
findMany: vi.fn().mockResolvedValue([{ id: 'test_1' }]),
|
|
111
|
-
count: vi.fn().mockResolvedValue(1),
|
|
112
|
-
updateMany: vi.fn().mockResolvedValue(undefined),
|
|
113
|
-
deleteMany: vi.fn().mockResolvedValue(undefined),
|
|
114
|
-
upsert: vi.fn().mockResolvedValue(undefined),
|
|
115
|
-
transaction: vi.fn().mockImplementation((fn: any) => fn(createMockOrm())),
|
|
116
|
-
} as any;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// A minimal where builder mock that captures calls for assertion
|
|
120
|
-
function createWhereBuilder() {
|
|
121
|
-
const builder: any = (col: string, op: string, val: any) => ({
|
|
122
|
-
_type: 'condition',
|
|
123
|
-
col,
|
|
124
|
-
op,
|
|
125
|
-
val,
|
|
126
|
-
});
|
|
127
|
-
builder.and = (...conditions: any[]) => ({
|
|
128
|
-
_type: 'and',
|
|
129
|
-
conditions,
|
|
130
|
-
});
|
|
131
|
-
builder.or = (...conditions: any[]) => ({
|
|
132
|
-
_type: 'or',
|
|
133
|
-
conditions,
|
|
134
|
-
});
|
|
135
|
-
builder.not = (v: any) => ({ _type: 'not', v });
|
|
136
|
-
builder.isNull = (a: string) => ({ _type: 'isNull', a });
|
|
137
|
-
builder.isNotNull = (a: string) => ({ _type: 'isNotNull', a });
|
|
138
|
-
return builder;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
describe('withTenantScope', () => {
|
|
142
|
-
const tenantId = 'tenant_abc';
|
|
143
|
-
|
|
144
|
-
describe('create', () => {
|
|
145
|
-
it('should inject tenantId into created data', async () => {
|
|
146
|
-
const db = createMockOrm();
|
|
147
|
-
const scoped = withTenantScope(db, tenantId);
|
|
148
|
-
|
|
149
|
-
await scoped.create('subject', {
|
|
150
|
-
id: 'sub_1',
|
|
151
|
-
externalId: null,
|
|
152
|
-
identityProvider: 'anonymous',
|
|
153
|
-
isIdentified: false,
|
|
154
|
-
} as any);
|
|
155
|
-
|
|
156
|
-
expect(db.create).toHaveBeenCalledWith('subject', {
|
|
157
|
-
id: 'sub_1',
|
|
158
|
-
externalId: null,
|
|
159
|
-
identityProvider: 'anonymous',
|
|
160
|
-
isIdentified: false,
|
|
161
|
-
tenantId: 'tenant_abc',
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe('createMany', () => {
|
|
167
|
-
it('should inject tenantId into all items', async () => {
|
|
168
|
-
const db = createMockOrm();
|
|
169
|
-
const scoped = withTenantScope(db, tenantId);
|
|
170
|
-
|
|
171
|
-
await scoped.createMany('subject', [
|
|
172
|
-
{ id: 'sub_1', isIdentified: false } as any,
|
|
173
|
-
{ id: 'sub_2', isIdentified: true } as any,
|
|
174
|
-
]);
|
|
175
|
-
|
|
176
|
-
expect(db.createMany).toHaveBeenCalledWith('subject', [
|
|
177
|
-
{ id: 'sub_1', isIdentified: false, tenantId: 'tenant_abc' },
|
|
178
|
-
{ id: 'sub_2', isIdentified: true, tenantId: 'tenant_abc' },
|
|
179
|
-
]);
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe('findFirst', () => {
|
|
184
|
-
it('should add tenantId filter to where clause', async () => {
|
|
185
|
-
const db = createMockOrm();
|
|
186
|
-
const scoped = withTenantScope(db, tenantId);
|
|
187
|
-
|
|
188
|
-
const originalWhere = (b: any) => b('id', '=', 'sub_1');
|
|
189
|
-
|
|
190
|
-
await scoped.findFirst('subject', {
|
|
191
|
-
where: originalWhere,
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
expect(db.findFirst).toHaveBeenCalledTimes(1);
|
|
195
|
-
const passedOpts = db.findFirst.mock.calls[0][1];
|
|
196
|
-
|
|
197
|
-
// Verify the where clause includes tenantId
|
|
198
|
-
const b = createWhereBuilder();
|
|
199
|
-
const result = passedOpts.where(b);
|
|
200
|
-
expect(result).toEqual({
|
|
201
|
-
_type: 'and',
|
|
202
|
-
conditions: [
|
|
203
|
-
{ _type: 'condition', col: 'id', op: '=', val: 'sub_1' },
|
|
204
|
-
{
|
|
205
|
-
_type: 'condition',
|
|
206
|
-
col: 'tenantId',
|
|
207
|
-
op: '=',
|
|
208
|
-
val: 'tenant_abc',
|
|
209
|
-
},
|
|
210
|
-
],
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('should use only tenantId filter when no original where', async () => {
|
|
215
|
-
const db = createMockOrm();
|
|
216
|
-
const scoped = withTenantScope(db, tenantId);
|
|
217
|
-
|
|
218
|
-
await scoped.findFirst('subject', {} as any);
|
|
219
|
-
|
|
220
|
-
const passedOpts = db.findFirst.mock.calls[0][1];
|
|
221
|
-
const b = createWhereBuilder();
|
|
222
|
-
const result = passedOpts.where(b);
|
|
223
|
-
expect(result).toEqual({
|
|
224
|
-
_type: 'condition',
|
|
225
|
-
col: 'tenantId',
|
|
226
|
-
op: '=',
|
|
227
|
-
val: 'tenant_abc',
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
describe('findMany', () => {
|
|
233
|
-
it('should add tenantId filter to where clause', async () => {
|
|
234
|
-
const db = createMockOrm();
|
|
235
|
-
const scoped = withTenantScope(db, tenantId);
|
|
236
|
-
|
|
237
|
-
await scoped.findMany('consent', {
|
|
238
|
-
where: (b: any) => b('subjectId', '=', 'sub_1'),
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
const passedOpts = db.findMany.mock.calls[0][1];
|
|
242
|
-
const b = createWhereBuilder();
|
|
243
|
-
const result = passedOpts.where(b);
|
|
244
|
-
expect(result).toEqual({
|
|
245
|
-
_type: 'and',
|
|
246
|
-
conditions: [
|
|
247
|
-
{
|
|
248
|
-
_type: 'condition',
|
|
249
|
-
col: 'subjectId',
|
|
250
|
-
op: '=',
|
|
251
|
-
val: 'sub_1',
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
_type: 'condition',
|
|
255
|
-
col: 'tenantId',
|
|
256
|
-
op: '=',
|
|
257
|
-
val: 'tenant_abc',
|
|
258
|
-
},
|
|
259
|
-
],
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it('should handle findMany with no options', async () => {
|
|
264
|
-
const db = createMockOrm();
|
|
265
|
-
const scoped = withTenantScope(db, tenantId);
|
|
266
|
-
|
|
267
|
-
await scoped.findMany('subject');
|
|
268
|
-
|
|
269
|
-
const passedOpts = db.findMany.mock.calls[0][1];
|
|
270
|
-
const b = createWhereBuilder();
|
|
271
|
-
const result = passedOpts.where(b);
|
|
272
|
-
expect(result).toEqual({
|
|
273
|
-
_type: 'condition',
|
|
274
|
-
col: 'tenantId',
|
|
275
|
-
op: '=',
|
|
276
|
-
val: 'tenant_abc',
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
describe('count', () => {
|
|
282
|
-
it('should add tenantId filter to where clause', async () => {
|
|
283
|
-
const db = createMockOrm();
|
|
284
|
-
const scoped = withTenantScope(db, tenantId);
|
|
285
|
-
|
|
286
|
-
await scoped.count('subject', {
|
|
287
|
-
where: (b: any) => b('isIdentified', '=', true),
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
const passedOpts = db.count.mock.calls[0][1];
|
|
291
|
-
const b = createWhereBuilder();
|
|
292
|
-
const result = passedOpts.where(b);
|
|
293
|
-
expect(result).toEqual({
|
|
294
|
-
_type: 'and',
|
|
295
|
-
conditions: [
|
|
296
|
-
{
|
|
297
|
-
_type: 'condition',
|
|
298
|
-
col: 'isIdentified',
|
|
299
|
-
op: '=',
|
|
300
|
-
val: true,
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
_type: 'condition',
|
|
304
|
-
col: 'tenantId',
|
|
305
|
-
op: '=',
|
|
306
|
-
val: 'tenant_abc',
|
|
307
|
-
},
|
|
308
|
-
],
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
describe('updateMany', () => {
|
|
314
|
-
it('should add tenantId filter to where clause', async () => {
|
|
315
|
-
const db = createMockOrm();
|
|
316
|
-
const scoped = withTenantScope(db, tenantId);
|
|
317
|
-
|
|
318
|
-
await scoped.updateMany('subject', {
|
|
319
|
-
where: (b: any) => b('id', '=', 'sub_1'),
|
|
320
|
-
set: { identityProvider: 'google' },
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
const passedOpts = db.updateMany.mock.calls[0][1];
|
|
324
|
-
|
|
325
|
-
// Verify set is preserved
|
|
326
|
-
expect(passedOpts.set).toEqual({ identityProvider: 'google' });
|
|
327
|
-
|
|
328
|
-
// Verify where includes tenantId
|
|
329
|
-
const b = createWhereBuilder();
|
|
330
|
-
const result = passedOpts.where(b);
|
|
331
|
-
expect(result).toEqual({
|
|
332
|
-
_type: 'and',
|
|
333
|
-
conditions: [
|
|
334
|
-
{ _type: 'condition', col: 'id', op: '=', val: 'sub_1' },
|
|
335
|
-
{
|
|
336
|
-
_type: 'condition',
|
|
337
|
-
col: 'tenantId',
|
|
338
|
-
op: '=',
|
|
339
|
-
val: 'tenant_abc',
|
|
340
|
-
},
|
|
341
|
-
],
|
|
342
|
-
});
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
describe('deleteMany', () => {
|
|
347
|
-
it('should add tenantId filter to where clause', async () => {
|
|
348
|
-
const db = createMockOrm();
|
|
349
|
-
const scoped = withTenantScope(db, tenantId);
|
|
350
|
-
|
|
351
|
-
await scoped.deleteMany('consent', {
|
|
352
|
-
where: (b: any) => b('id', '=', 'cns_1'),
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
const passedOpts = db.deleteMany.mock.calls[0][1];
|
|
356
|
-
const b = createWhereBuilder();
|
|
357
|
-
const result = passedOpts.where(b);
|
|
358
|
-
expect(result).toEqual({
|
|
359
|
-
_type: 'and',
|
|
360
|
-
conditions: [
|
|
361
|
-
{ _type: 'condition', col: 'id', op: '=', val: 'cns_1' },
|
|
362
|
-
{
|
|
363
|
-
_type: 'condition',
|
|
364
|
-
col: 'tenantId',
|
|
365
|
-
op: '=',
|
|
366
|
-
val: 'tenant_abc',
|
|
367
|
-
},
|
|
368
|
-
],
|
|
369
|
-
});
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
describe('upsert', () => {
|
|
374
|
-
it('should add tenantId to where clause and create data', async () => {
|
|
375
|
-
const db = createMockOrm();
|
|
376
|
-
const scoped = withTenantScope(db, tenantId);
|
|
377
|
-
|
|
378
|
-
await scoped.upsert('subject', {
|
|
379
|
-
where: (b: any) => b('externalId', '=', 'ext_1'),
|
|
380
|
-
create: {
|
|
381
|
-
id: 'sub_1',
|
|
382
|
-
externalId: 'ext_1',
|
|
383
|
-
isIdentified: true,
|
|
384
|
-
} as any,
|
|
385
|
-
update: { identityProvider: 'google' },
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
const passedOpts = db.upsert.mock.calls[0][1];
|
|
389
|
-
|
|
390
|
-
// Verify create includes tenantId
|
|
391
|
-
expect(passedOpts.create).toEqual({
|
|
392
|
-
id: 'sub_1',
|
|
393
|
-
externalId: 'ext_1',
|
|
394
|
-
isIdentified: true,
|
|
395
|
-
tenantId: 'tenant_abc',
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
// Verify update is preserved
|
|
399
|
-
expect(passedOpts.update).toEqual({ identityProvider: 'google' });
|
|
400
|
-
|
|
401
|
-
// Verify where includes tenantId
|
|
402
|
-
const b = createWhereBuilder();
|
|
403
|
-
const result = passedOpts.where(b);
|
|
404
|
-
expect(result).toEqual({
|
|
405
|
-
_type: 'and',
|
|
406
|
-
conditions: [
|
|
407
|
-
{
|
|
408
|
-
_type: 'condition',
|
|
409
|
-
col: 'externalId',
|
|
410
|
-
op: '=',
|
|
411
|
-
val: 'ext_1',
|
|
412
|
-
},
|
|
413
|
-
{
|
|
414
|
-
_type: 'condition',
|
|
415
|
-
col: 'tenantId',
|
|
416
|
-
op: '=',
|
|
417
|
-
val: 'tenant_abc',
|
|
418
|
-
},
|
|
419
|
-
],
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
describe('transaction', () => {
|
|
425
|
-
it('should provide a tenant-scoped ORM inside transaction', async () => {
|
|
426
|
-
const innerDb = createMockOrm();
|
|
427
|
-
const db = createMockOrm();
|
|
428
|
-
db.transaction.mockImplementation((fn: any) => fn(innerDb));
|
|
429
|
-
|
|
430
|
-
const scoped = withTenantScope(db, tenantId);
|
|
431
|
-
|
|
432
|
-
await scoped.transaction(async (tx) => {
|
|
433
|
-
await tx.create('subject', {
|
|
434
|
-
id: 'sub_1',
|
|
435
|
-
isIdentified: false,
|
|
436
|
-
} as any);
|
|
437
|
-
await tx.findFirst('subject', {
|
|
438
|
-
where: (b: any) => b('id', '=', 'sub_1'),
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
// The inner db should have tenantId injected
|
|
443
|
-
expect(innerDb.create).toHaveBeenCalledWith('subject', {
|
|
444
|
-
id: 'sub_1',
|
|
445
|
-
isIdentified: false,
|
|
446
|
-
tenantId: 'tenant_abc',
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
const findOpts = innerDb.findFirst.mock.calls[0][1];
|
|
450
|
-
const b = createWhereBuilder();
|
|
451
|
-
const result = findOpts.where(b);
|
|
452
|
-
expect(result).toEqual({
|
|
453
|
-
_type: 'and',
|
|
454
|
-
conditions: [
|
|
455
|
-
{ _type: 'condition', col: 'id', op: '=', val: 'sub_1' },
|
|
456
|
-
{
|
|
457
|
-
_type: 'condition',
|
|
458
|
-
col: 'tenantId',
|
|
459
|
-
op: '=',
|
|
460
|
-
val: 'tenant_abc',
|
|
461
|
-
},
|
|
462
|
-
],
|
|
463
|
-
});
|
|
464
|
-
});
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
describe('data isolation (mock)', () => {
|
|
468
|
-
it('should scope different tenants to their own data', async () => {
|
|
469
|
-
const db = createMockOrm();
|
|
470
|
-
const tenantA = withTenantScope(db, 'tenant_a');
|
|
471
|
-
const tenantB = withTenantScope(db, 'tenant_b');
|
|
472
|
-
|
|
473
|
-
await tenantA.create('subject', { id: 'sub_1' } as any);
|
|
474
|
-
await tenantB.create('subject', { id: 'sub_2' } as any);
|
|
475
|
-
|
|
476
|
-
expect(db.create).toHaveBeenCalledWith('subject', {
|
|
477
|
-
id: 'sub_1',
|
|
478
|
-
tenantId: 'tenant_a',
|
|
479
|
-
});
|
|
480
|
-
expect(db.create).toHaveBeenCalledWith('subject', {
|
|
481
|
-
id: 'sub_2',
|
|
482
|
-
tenantId: 'tenant_b',
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
// Each tenant's findMany should have its own tenantId filter
|
|
486
|
-
await tenantA.findMany('subject');
|
|
487
|
-
await tenantB.findMany('subject');
|
|
488
|
-
|
|
489
|
-
const b = createWhereBuilder();
|
|
490
|
-
|
|
491
|
-
const tenantAOpts = db.findMany.mock.calls[0][1];
|
|
492
|
-
expect(tenantAOpts.where(b)).toEqual({
|
|
493
|
-
_type: 'condition',
|
|
494
|
-
col: 'tenantId',
|
|
495
|
-
op: '=',
|
|
496
|
-
val: 'tenant_a',
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
const tenantBOpts = db.findMany.mock.calls[1][1];
|
|
500
|
-
expect(tenantBOpts.where(b)).toEqual({
|
|
501
|
-
_type: 'condition',
|
|
502
|
-
col: 'tenantId',
|
|
503
|
-
op: '=',
|
|
504
|
-
val: 'tenant_b',
|
|
505
|
-
});
|
|
506
|
-
});
|
|
507
|
-
});
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
// ===========================================================================
|
|
511
|
-
// Integration tests — real in-memory store proving row-level isolation
|
|
512
|
-
// ===========================================================================
|
|
513
|
-
|
|
514
|
-
describe('withTenantScope – row-level isolation (integration)', () => {
|
|
515
|
-
it('tenant A cannot see subjects created by tenant B', async () => {
|
|
516
|
-
const { orm } = createInMemoryOrm();
|
|
517
|
-
const tenantA = withTenantScope(orm, 'tenant_a');
|
|
518
|
-
const tenantB = withTenantScope(orm, 'tenant_b');
|
|
519
|
-
|
|
520
|
-
await tenantA.create('subject', {
|
|
521
|
-
id: 'sub_1',
|
|
522
|
-
externalId: 'Alice',
|
|
523
|
-
} as any);
|
|
524
|
-
await tenantB.create('subject', { id: 'sub_2', externalId: 'Bob' } as any);
|
|
525
|
-
|
|
526
|
-
const aSubjects = await tenantA.findMany('subject');
|
|
527
|
-
const bSubjects = await tenantB.findMany('subject');
|
|
528
|
-
|
|
529
|
-
expect(aSubjects).toHaveLength(1);
|
|
530
|
-
expect(aSubjects[0]!.id).toBe('sub_1');
|
|
531
|
-
expect(aSubjects[0]!.externalId).toBe('Alice');
|
|
532
|
-
|
|
533
|
-
expect(bSubjects).toHaveLength(1);
|
|
534
|
-
expect(bSubjects[0]!.id).toBe('sub_2');
|
|
535
|
-
expect(bSubjects[0]!.externalId).toBe('Bob');
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
it('findFirst only returns records belonging to the querying tenant', async () => {
|
|
539
|
-
const { orm } = createInMemoryOrm();
|
|
540
|
-
const tenantA = withTenantScope(orm, 'tenant_a');
|
|
541
|
-
const tenantB = withTenantScope(orm, 'tenant_b');
|
|
542
|
-
|
|
543
|
-
await tenantA.create('consent', { id: 'cns_1', subjectId: 'sub_1' } as any);
|
|
544
|
-
|
|
545
|
-
const fromA = await tenantA.findFirst('consent', {
|
|
546
|
-
where: (b: any) => b('id', '=', 'cns_1'),
|
|
547
|
-
});
|
|
548
|
-
const fromB = await tenantB.findFirst('consent', {
|
|
549
|
-
where: (b: any) => b('id', '=', 'cns_1'),
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
expect(fromA).not.toBeNull();
|
|
553
|
-
expect(fromA!.id).toBe('cns_1');
|
|
554
|
-
expect(fromB).toBeNull();
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
it('count only counts records belonging to the querying tenant', async () => {
|
|
558
|
-
const { orm } = createInMemoryOrm();
|
|
559
|
-
const tenantA = withTenantScope(orm, 'tenant_a');
|
|
560
|
-
const tenantB = withTenantScope(orm, 'tenant_b');
|
|
561
|
-
|
|
562
|
-
await tenantA.create('subject', { id: 'sub_1' } as any);
|
|
563
|
-
await tenantA.create('subject', { id: 'sub_2' } as any);
|
|
564
|
-
await tenantB.create('subject', { id: 'sub_3' } as any);
|
|
565
|
-
|
|
566
|
-
expect(await tenantA.count('subject')).toBe(2);
|
|
567
|
-
expect(await tenantB.count('subject')).toBe(1);
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
it("updateMany only affects the calling tenant's rows", async () => {
|
|
571
|
-
const { orm } = createInMemoryOrm();
|
|
572
|
-
const tenantA = withTenantScope(orm, 'tenant_a');
|
|
573
|
-
const tenantB = withTenantScope(orm, 'tenant_b');
|
|
574
|
-
|
|
575
|
-
await tenantA.create('subject', {
|
|
576
|
-
id: 'sub_1',
|
|
577
|
-
identityProvider: '1.1.1.1',
|
|
578
|
-
} as any);
|
|
579
|
-
await tenantB.create('subject', {
|
|
580
|
-
id: 'sub_2',
|
|
581
|
-
identityProvider: '2.2.2.2',
|
|
582
|
-
} as any);
|
|
583
|
-
|
|
584
|
-
// Tenant A updates all their subjects
|
|
585
|
-
await tenantA.updateMany('subject', {
|
|
586
|
-
where: (b: any) => b('id', '=', 'sub_1'),
|
|
587
|
-
set: { identityProvider: '9.9.9.9' },
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
// Tenant A's row is updated
|
|
591
|
-
const aRow = await tenantA.findFirst('subject', {
|
|
592
|
-
where: (b: any) => b('id', '=', 'sub_1'),
|
|
593
|
-
});
|
|
594
|
-
expect(aRow!.identityProvider).toBe('9.9.9.9');
|
|
595
|
-
|
|
596
|
-
// Tenant B's row is untouched
|
|
597
|
-
const bRow = await tenantB.findFirst('subject', {
|
|
598
|
-
where: (b: any) => b('id', '=', 'sub_2'),
|
|
599
|
-
});
|
|
600
|
-
expect(bRow!.identityProvider).toBe('2.2.2.2');
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
it("deleteMany only deletes the calling tenant's rows", async () => {
|
|
604
|
-
const { orm } = createInMemoryOrm();
|
|
605
|
-
const tenantA = withTenantScope(orm, 'tenant_a');
|
|
606
|
-
const tenantB = withTenantScope(orm, 'tenant_b');
|
|
607
|
-
|
|
608
|
-
await tenantA.create('domain', { id: 'dom_1', name: 'a.com' } as any);
|
|
609
|
-
await tenantB.create('domain', { id: 'dom_2', name: 'b.com' } as any);
|
|
610
|
-
|
|
611
|
-
// Tenant A deletes their domain
|
|
612
|
-
await tenantA.deleteMany('domain', {
|
|
613
|
-
where: (b: any) => b('id', '=', 'dom_1'),
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
expect(await tenantA.findMany('domain')).toHaveLength(0);
|
|
617
|
-
expect(await tenantB.findMany('domain')).toHaveLength(1);
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
it('upsert creates with tenantId and only matches own rows', async () => {
|
|
621
|
-
const { orm } = createInMemoryOrm();
|
|
622
|
-
const tenantA = withTenantScope(orm, 'tenant_a');
|
|
623
|
-
const tenantB = withTenantScope(orm, 'tenant_b');
|
|
624
|
-
|
|
625
|
-
// Tenant A upserts — creates since row doesn't exist
|
|
626
|
-
await tenantA.upsert('subject', {
|
|
627
|
-
where: (b: any) => b('externalId', '=', 'ext_1'),
|
|
628
|
-
create: {
|
|
629
|
-
id: 'sub_1',
|
|
630
|
-
externalId: 'ext_1',
|
|
631
|
-
identityProvider: '1.1.1.1',
|
|
632
|
-
} as any,
|
|
633
|
-
update: { identityProvider: '9.9.9.9' },
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
// Tenant B upserts same externalId — should also CREATE (not update A's row)
|
|
637
|
-
await tenantB.upsert('subject', {
|
|
638
|
-
where: (b: any) => b('externalId', '=', 'ext_1'),
|
|
639
|
-
create: {
|
|
640
|
-
id: 'sub_2',
|
|
641
|
-
externalId: 'ext_1',
|
|
642
|
-
identityProvider: '2.2.2.2',
|
|
643
|
-
} as any,
|
|
644
|
-
update: { identityProvider: '8.8.8.8' },
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
const aRows = await tenantA.findMany('subject');
|
|
648
|
-
const bRows = await tenantB.findMany('subject');
|
|
649
|
-
|
|
650
|
-
// Each tenant has their own row, even though externalId is the same
|
|
651
|
-
expect(aRows).toHaveLength(1);
|
|
652
|
-
expect(aRows[0]!.id).toBe('sub_1');
|
|
653
|
-
expect(aRows[0]!.identityProvider).toBe('1.1.1.1'); // unchanged
|
|
654
|
-
|
|
655
|
-
expect(bRows).toHaveLength(1);
|
|
656
|
-
expect(bRows[0]!.id).toBe('sub_2');
|
|
657
|
-
expect(bRows[0]!.identityProvider).toBe('2.2.2.2'); // created, not updated from A
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
it("tenant A cannot update tenant B's row even with matching id", async () => {
|
|
661
|
-
const { orm } = createInMemoryOrm();
|
|
662
|
-
const tenantA = withTenantScope(orm, 'tenant_a');
|
|
663
|
-
const tenantB = withTenantScope(orm, 'tenant_b');
|
|
664
|
-
|
|
665
|
-
await tenantB.create('subject', {
|
|
666
|
-
id: 'sub_1',
|
|
667
|
-
identityProvider: '2.2.2.2',
|
|
668
|
-
} as any);
|
|
669
|
-
|
|
670
|
-
// Tenant A tries to update sub_1 which belongs to B
|
|
671
|
-
await tenantA.updateMany('subject', {
|
|
672
|
-
where: (b: any) => b('id', '=', 'sub_1'),
|
|
673
|
-
set: { identityProvider: 'hacked' },
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
// B's row is still untouched
|
|
677
|
-
const bRow = await tenantB.findFirst('subject', {
|
|
678
|
-
where: (b: any) => b('id', '=', 'sub_1'),
|
|
679
|
-
});
|
|
680
|
-
expect(bRow!.identityProvider).toBe('2.2.2.2');
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
it("tenant A cannot delete tenant B's row even with matching id", async () => {
|
|
684
|
-
const { orm } = createInMemoryOrm();
|
|
685
|
-
const tenantA = withTenantScope(orm, 'tenant_a');
|
|
686
|
-
const tenantB = withTenantScope(orm, 'tenant_b');
|
|
687
|
-
|
|
688
|
-
await tenantB.create('domain', { id: 'dom_1', name: 'b.com' } as any);
|
|
689
|
-
|
|
690
|
-
// Tenant A tries to delete dom_1 which belongs to B
|
|
691
|
-
await tenantA.deleteMany('domain', {
|
|
692
|
-
where: (b: any) => b('id', '=', 'dom_1'),
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
// B's row still exists
|
|
696
|
-
expect(await tenantB.findMany('domain')).toHaveLength(1);
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
it('transaction inherits tenant scope', async () => {
|
|
700
|
-
const { orm } = createInMemoryOrm();
|
|
701
|
-
const tenantA = withTenantScope(orm, 'tenant_a');
|
|
702
|
-
const tenantB = withTenantScope(orm, 'tenant_b');
|
|
703
|
-
|
|
704
|
-
await tenantA.transaction(async (tx) => {
|
|
705
|
-
await tx.create('subject', {
|
|
706
|
-
id: 'sub_tx',
|
|
707
|
-
externalId: 'TxAlice',
|
|
708
|
-
} as any);
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
// Visible to tenant A
|
|
712
|
-
const aResult = await tenantA.findFirst('subject', {
|
|
713
|
-
where: (b: any) => b('id', '=', 'sub_tx'),
|
|
714
|
-
});
|
|
715
|
-
expect(aResult).not.toBeNull();
|
|
716
|
-
expect(aResult!.externalId).toBe('TxAlice');
|
|
717
|
-
|
|
718
|
-
// Invisible to tenant B
|
|
719
|
-
const bResult = await tenantB.findFirst('subject', {
|
|
720
|
-
where: (b: any) => b('id', '=', 'sub_tx'),
|
|
721
|
-
});
|
|
722
|
-
expect(bResult).toBeNull();
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
it('three tenants sharing same database are fully isolated', async () => {
|
|
726
|
-
const { orm, store } = createInMemoryOrm();
|
|
727
|
-
const t1 = withTenantScope(orm, 'inst_001');
|
|
728
|
-
const t2 = withTenantScope(orm, 'inst_002');
|
|
729
|
-
const t3 = withTenantScope(orm, 'inst_003');
|
|
730
|
-
|
|
731
|
-
await t1.create('subject', { id: 'sub_1' } as any);
|
|
732
|
-
await t2.create('subject', { id: 'sub_2' } as any);
|
|
733
|
-
await t2.create('subject', { id: 'sub_3' } as any);
|
|
734
|
-
await t3.create('subject', { id: 'sub_4' } as any);
|
|
735
|
-
await t3.create('subject', { id: 'sub_5' } as any);
|
|
736
|
-
await t3.create('subject', { id: 'sub_6' } as any);
|
|
737
|
-
|
|
738
|
-
// Underlying store has all 6 rows
|
|
739
|
-
expect(store.get('subject')).toHaveLength(6);
|
|
740
|
-
|
|
741
|
-
// But each tenant only sees their own
|
|
742
|
-
expect(await t1.count('subject')).toBe(1);
|
|
743
|
-
expect(await t2.count('subject')).toBe(2);
|
|
744
|
-
expect(await t3.count('subject')).toBe(3);
|
|
745
|
-
|
|
746
|
-
expect(await t1.findMany('subject')).toHaveLength(1);
|
|
747
|
-
expect(await t2.findMany('subject')).toHaveLength(2);
|
|
748
|
-
expect(await t3.findMany('subject')).toHaveLength(3);
|
|
749
|
-
});
|
|
750
|
-
});
|