@c15t/backend 2.0.0-rc.4 → 2.0.0-rc.5

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