@c15t/backend 2.0.0-rc.3 → 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 (314) hide show
  1. package/dist/cache.cjs +4 -4
  2. package/dist/cache.js +4 -4
  3. package/dist/core.cjs +845 -87
  4. package/dist/core.js +821 -87
  5. package/dist/db/schema.cjs +37 -0
  6. package/dist/db/schema.js +33 -2
  7. package/dist/edge.cjs +1106 -0
  8. package/dist/edge.js +1069 -0
  9. package/dist/router.cjs +621 -71
  10. package/dist/router.js +621 -71
  11. package/{dist → dist-types}/cache/adapters/cloudflare-kv.d.ts +0 -1
  12. package/{dist → dist-types}/cache/adapters/index.d.ts +0 -1
  13. package/{dist → dist-types}/cache/adapters/memory.d.ts +0 -1
  14. package/{dist → dist-types}/cache/adapters/upstash-redis.d.ts +0 -1
  15. package/{dist → dist-types}/cache/gvl-resolver.d.ts +1 -2
  16. package/{dist → dist-types}/cache/index.d.ts +0 -1
  17. package/{dist → dist-types}/cache/keys.d.ts +0 -1
  18. package/{dist → dist-types}/cache/types.d.ts +0 -1
  19. package/{dist → dist-types}/core.d.ts +8 -1
  20. package/{dist → dist-types}/db/migrator/index.d.ts +0 -1
  21. package/{dist → dist-types}/db/registry/consent-policy.d.ts +0 -1
  22. package/{dist → dist-types}/db/registry/consent-purpose.d.ts +0 -1
  23. package/{dist → dist-types}/db/registry/domain.d.ts +0 -1
  24. package/{dist → dist-types}/db/registry/index.d.ts +22 -2
  25. package/dist-types/db/registry/runtime-policy-decision.d.ts +60 -0
  26. package/{dist → dist-types}/db/registry/subject.d.ts +0 -1
  27. package/{dist → dist-types}/db/registry/types.d.ts +1 -2
  28. package/{dist → dist-types}/db/registry/utils/generate-id.d.ts +0 -1
  29. package/{dist → dist-types}/db/registry/utils.d.ts +0 -1
  30. package/{dist → dist-types}/db/schema/1.0.0/audit-log.d.ts +0 -1
  31. package/{dist → dist-types}/db/schema/1.0.0/consent-policy.d.ts +0 -1
  32. package/{dist → dist-types}/db/schema/1.0.0/consent-purpose.d.ts +0 -1
  33. package/{dist → dist-types}/db/schema/1.0.0/consent-record.d.ts +0 -1
  34. package/{dist → dist-types}/db/schema/1.0.0/consent.d.ts +1 -2
  35. package/{dist → dist-types}/db/schema/1.0.0/domain.d.ts +0 -1
  36. package/{dist → dist-types}/db/schema/1.0.0/index.d.ts +0 -1
  37. package/{dist → dist-types}/db/schema/1.0.0/subject.d.ts +0 -1
  38. package/{dist → dist-types}/db/schema/2.0.0/audit-log.d.ts +1 -2
  39. package/{dist → dist-types}/db/schema/2.0.0/consent-policy.d.ts +1 -2
  40. package/{dist → dist-types}/db/schema/2.0.0/consent-purpose.d.ts +1 -2
  41. package/{dist → dist-types}/db/schema/2.0.0/consent.d.ts +5 -2
  42. package/{dist → dist-types}/db/schema/2.0.0/domain.d.ts +1 -2
  43. package/{dist → dist-types}/db/schema/2.0.0/index.d.ts +432 -17
  44. package/dist-types/db/schema/2.0.0/runtime-policy-decision.d.ts +23 -0
  45. package/{dist → dist-types}/db/schema/2.0.0/subject.d.ts +1 -2
  46. package/{dist → dist-types}/db/schema/index.d.ts +862 -33
  47. package/{dist → dist-types}/db/tenant-scope.d.ts +0 -1
  48. package/dist-types/define-config.d.ts +17 -0
  49. package/dist-types/edge/index.d.ts +5 -0
  50. package/dist-types/edge/init-handler.d.ts +38 -0
  51. package/dist-types/edge/resolve-consent.d.ts +80 -0
  52. package/dist-types/edge/types.d.ts +13 -0
  53. package/{dist → dist-types}/handlers/consent/check.handler.d.ts +0 -1
  54. package/{src/handlers/consent/index.ts → dist-types/handlers/consent/index.d.ts} +0 -1
  55. package/{dist → dist-types}/handlers/init/geo.d.ts +2 -3
  56. package/{dist → dist-types}/handlers/init/index.d.ts +4 -5
  57. package/dist-types/handlers/init/policy.d.ts +26 -0
  58. package/dist-types/handlers/init/resolve-init.d.ts +44 -0
  59. package/dist-types/handlers/init/translations.d.ts +48 -0
  60. package/dist-types/handlers/policy/snapshot.d.ts +99 -0
  61. package/{src/handlers/status/index.ts → dist-types/handlers/status/index.d.ts} +0 -1
  62. package/{dist → dist-types}/handlers/status/status.handler.d.ts +0 -1
  63. package/{dist → dist-types}/handlers/subject/get.handler.d.ts +0 -1
  64. package/{src/handlers/subject/index.ts → dist-types/handlers/subject/index.d.ts} +0 -1
  65. package/{dist → dist-types}/handlers/subject/list.handler.d.ts +0 -1
  66. package/{dist → dist-types}/handlers/subject/patch.handler.d.ts +0 -1
  67. package/{dist → dist-types}/handlers/subject/post.handler.d.ts +12 -1
  68. package/{dist → dist-types}/handlers/utils/consent-enrichment.d.ts +0 -1
  69. package/{dist → dist-types}/init.d.ts +4 -7
  70. package/{dist → dist-types}/middleware/auth/index.d.ts +0 -1
  71. package/{dist → dist-types}/middleware/auth/validate-api-key.d.ts +0 -1
  72. package/{dist → dist-types}/middleware/cors/cors.d.ts +0 -1
  73. package/{src/middleware/cors/index.ts → dist-types/middleware/cors/index.d.ts} +0 -1
  74. package/{dist → dist-types}/middleware/cors/is-origin-trusted.d.ts +1 -2
  75. package/{dist → dist-types}/middleware/cors/process-cors.d.ts +0 -1
  76. package/{dist → dist-types}/middleware/openapi/config.d.ts +0 -1
  77. package/{dist → dist-types}/middleware/openapi/handlers.d.ts +0 -1
  78. package/{src/middleware/openapi/index.ts → dist-types/middleware/openapi/index.d.ts} +0 -1
  79. package/{dist → dist-types}/middleware/process-ip/index.d.ts +0 -1
  80. package/dist-types/policies/builder.d.ts +127 -0
  81. package/dist-types/policies/defaults.d.ts +2 -0
  82. package/dist-types/policies/matchers.d.ts +3 -0
  83. package/{dist → dist-types}/router.d.ts +0 -1
  84. package/{dist → dist-types}/routes/consent.d.ts +0 -1
  85. package/{src/routes/index.ts → dist-types/routes/index.d.ts} +0 -1
  86. package/{dist → dist-types}/routes/init.d.ts +0 -1
  87. package/{dist → dist-types}/routes/status.d.ts +0 -1
  88. package/{dist → dist-types}/routes/subject.d.ts +0 -1
  89. package/{dist → dist-types}/types/api.d.ts +0 -1
  90. package/dist-types/types/index.d.ts +443 -0
  91. package/dist-types/utils/background.d.ts +6 -0
  92. package/{dist → dist-types}/utils/create-telemetry-options.d.ts +1 -2
  93. package/{dist → dist-types}/utils/env.d.ts +0 -1
  94. package/{dist → dist-types}/utils/extract-error-message.d.ts +0 -1
  95. package/{dist → dist-types}/utils/instrumentation.d.ts +0 -1
  96. package/{dist → dist-types}/utils/logger.d.ts +1 -2
  97. package/{dist → dist-types}/utils/metrics.d.ts +0 -1
  98. package/dist-types/version.d.ts +1 -0
  99. package/docs/README.md +49 -0
  100. package/docs/api/configuration.md +197 -0
  101. package/docs/api/endpoints.md +211 -0
  102. package/docs/guides/caching.md +85 -0
  103. package/docs/guides/database-setup.md +128 -0
  104. package/docs/guides/edge-deployment.md +248 -0
  105. package/docs/guides/framework-integration.md +142 -0
  106. package/docs/guides/iab-tcf.md +89 -0
  107. package/docs/guides/observability.md +96 -0
  108. package/docs/guides/policy-packs.md +396 -0
  109. package/docs/quickstart.md +129 -0
  110. package/package.json +37 -23
  111. package/.turbo/turbo-build.log +0 -49
  112. package/CHANGELOG.md +0 -115
  113. package/dist/cache/adapters/cloudflare-kv.d.ts.map +0 -1
  114. package/dist/cache/adapters/index.d.ts.map +0 -1
  115. package/dist/cache/adapters/memory.d.ts.map +0 -1
  116. package/dist/cache/adapters/upstash-redis.d.ts.map +0 -1
  117. package/dist/cache/gvl-resolver.d.ts.map +0 -1
  118. package/dist/cache/index.d.ts.map +0 -1
  119. package/dist/cache/keys.d.ts.map +0 -1
  120. package/dist/cache/types.d.ts.map +0 -1
  121. package/dist/core.d.ts.map +0 -1
  122. package/dist/db/adapters/drizzle.d.ts +0 -2
  123. package/dist/db/adapters/drizzle.d.ts.map +0 -1
  124. package/dist/db/adapters/index.d.ts +0 -2
  125. package/dist/db/adapters/index.d.ts.map +0 -1
  126. package/dist/db/adapters/kysely.d.ts +0 -2
  127. package/dist/db/adapters/kysely.d.ts.map +0 -1
  128. package/dist/db/adapters/mongo.d.ts +0 -2
  129. package/dist/db/adapters/mongo.d.ts.map +0 -1
  130. package/dist/db/adapters/prisma.d.ts +0 -2
  131. package/dist/db/adapters/prisma.d.ts.map +0 -1
  132. package/dist/db/adapters/typeorm.d.ts +0 -2
  133. package/dist/db/adapters/typeorm.d.ts.map +0 -1
  134. package/dist/db/migrator/index.d.ts.map +0 -1
  135. package/dist/db/registry/consent-policy.d.ts.map +0 -1
  136. package/dist/db/registry/consent-purpose.d.ts.map +0 -1
  137. package/dist/db/registry/domain.d.ts.map +0 -1
  138. package/dist/db/registry/index.d.ts.map +0 -1
  139. package/dist/db/registry/subject.d.ts.map +0 -1
  140. package/dist/db/registry/types.d.ts.map +0 -1
  141. package/dist/db/registry/utils/generate-id.d.ts.map +0 -1
  142. package/dist/db/registry/utils.d.ts.map +0 -1
  143. package/dist/db/schema/1.0.0/audit-log.d.ts.map +0 -1
  144. package/dist/db/schema/1.0.0/consent-policy.d.ts.map +0 -1
  145. package/dist/db/schema/1.0.0/consent-purpose.d.ts.map +0 -1
  146. package/dist/db/schema/1.0.0/consent-record.d.ts.map +0 -1
  147. package/dist/db/schema/1.0.0/consent.d.ts.map +0 -1
  148. package/dist/db/schema/1.0.0/domain.d.ts.map +0 -1
  149. package/dist/db/schema/1.0.0/index.d.ts.map +0 -1
  150. package/dist/db/schema/1.0.0/subject.d.ts.map +0 -1
  151. package/dist/db/schema/2.0.0/audit-log.d.ts.map +0 -1
  152. package/dist/db/schema/2.0.0/consent-policy.d.ts.map +0 -1
  153. package/dist/db/schema/2.0.0/consent-purpose.d.ts.map +0 -1
  154. package/dist/db/schema/2.0.0/consent.d.ts.map +0 -1
  155. package/dist/db/schema/2.0.0/domain.d.ts.map +0 -1
  156. package/dist/db/schema/2.0.0/index.d.ts.map +0 -1
  157. package/dist/db/schema/2.0.0/subject.d.ts.map +0 -1
  158. package/dist/db/schema/index.d.ts.map +0 -1
  159. package/dist/db/tenant-scope.d.ts.map +0 -1
  160. package/dist/define-config.d.ts +0 -5
  161. package/dist/define-config.d.ts.map +0 -1
  162. package/dist/handlers/consent/check.handler.d.ts.map +0 -1
  163. package/dist/handlers/consent/index.d.ts +0 -12
  164. package/dist/handlers/consent/index.d.ts.map +0 -1
  165. package/dist/handlers/init/geo.d.ts.map +0 -1
  166. package/dist/handlers/init/index.d.ts.map +0 -1
  167. package/dist/handlers/init/translations.d.ts +0 -28
  168. package/dist/handlers/init/translations.d.ts.map +0 -1
  169. package/dist/handlers/status/index.d.ts +0 -7
  170. package/dist/handlers/status/index.d.ts.map +0 -1
  171. package/dist/handlers/status/status.handler.d.ts.map +0 -1
  172. package/dist/handlers/subject/get.handler.d.ts.map +0 -1
  173. package/dist/handlers/subject/index.d.ts +0 -10
  174. package/dist/handlers/subject/index.d.ts.map +0 -1
  175. package/dist/handlers/subject/list.handler.d.ts.map +0 -1
  176. package/dist/handlers/subject/patch.handler.d.ts.map +0 -1
  177. package/dist/handlers/subject/post.handler.d.ts.map +0 -1
  178. package/dist/handlers/utils/consent-enrichment.d.ts.map +0 -1
  179. package/dist/init.d.ts.map +0 -1
  180. package/dist/middleware/auth/index.d.ts.map +0 -1
  181. package/dist/middleware/auth/validate-api-key.d.ts.map +0 -1
  182. package/dist/middleware/cors/cors.d.ts.map +0 -1
  183. package/dist/middleware/cors/index.d.ts +0 -30
  184. package/dist/middleware/cors/index.d.ts.map +0 -1
  185. package/dist/middleware/cors/is-origin-trusted.d.ts.map +0 -1
  186. package/dist/middleware/cors/process-cors.d.ts.map +0 -1
  187. package/dist/middleware/openapi/config.d.ts.map +0 -1
  188. package/dist/middleware/openapi/handlers.d.ts.map +0 -1
  189. package/dist/middleware/openapi/index.d.ts +0 -12
  190. package/dist/middleware/openapi/index.d.ts.map +0 -1
  191. package/dist/middleware/process-ip/index.d.ts.map +0 -1
  192. package/dist/router.d.ts.map +0 -1
  193. package/dist/routes/consent.d.ts.map +0 -1
  194. package/dist/routes/index.d.ts +0 -10
  195. package/dist/routes/index.d.ts.map +0 -1
  196. package/dist/routes/init.d.ts.map +0 -1
  197. package/dist/routes/status.d.ts.map +0 -1
  198. package/dist/routes/subject.d.ts.map +0 -1
  199. package/dist/types/api.d.ts.map +0 -1
  200. package/dist/types/index.d.ts +0 -263
  201. package/dist/types/index.d.ts.map +0 -1
  202. package/dist/utils/create-telemetry-options.d.ts.map +0 -1
  203. package/dist/utils/env.d.ts.map +0 -1
  204. package/dist/utils/extract-error-message.d.ts.map +0 -1
  205. package/dist/utils/index.d.ts +0 -4
  206. package/dist/utils/index.d.ts.map +0 -1
  207. package/dist/utils/instrumentation.d.ts.map +0 -1
  208. package/dist/utils/logger.d.ts.map +0 -1
  209. package/dist/utils/metrics.d.ts.map +0 -1
  210. package/dist/version.d.ts +0 -2
  211. package/dist/version.d.ts.map +0 -1
  212. package/knip.json +0 -31
  213. package/rslib.config.ts +0 -93
  214. package/src/cache/adapters/cloudflare-kv.ts +0 -71
  215. package/src/cache/adapters/index.ts +0 -22
  216. package/src/cache/adapters/memory.ts +0 -111
  217. package/src/cache/adapters/upstash-redis.ts +0 -113
  218. package/src/cache/gvl-resolver.ts +0 -289
  219. package/src/cache/index.ts +0 -34
  220. package/src/cache/keys.ts +0 -68
  221. package/src/cache/types.ts +0 -66
  222. package/src/core.ts +0 -369
  223. package/src/db/migrator/index.ts +0 -80
  224. package/src/db/registry/consent-policy.test.ts +0 -451
  225. package/src/db/registry/consent-policy.ts +0 -82
  226. package/src/db/registry/consent-purpose.test.ts +0 -428
  227. package/src/db/registry/consent-purpose.ts +0 -61
  228. package/src/db/registry/domain.test.ts +0 -445
  229. package/src/db/registry/domain.ts +0 -91
  230. package/src/db/registry/index.ts +0 -14
  231. package/src/db/registry/subject.test.ts +0 -371
  232. package/src/db/registry/subject.ts +0 -126
  233. package/src/db/registry/types.ts +0 -10
  234. package/src/db/registry/utils/generate-id.test.ts +0 -216
  235. package/src/db/registry/utils/generate-id.ts +0 -133
  236. package/src/db/registry/utils.ts +0 -133
  237. package/src/db/schema/1.0.0/audit-log.ts +0 -15
  238. package/src/db/schema/1.0.0/consent-policy.ts +0 -14
  239. package/src/db/schema/1.0.0/consent-purpose.ts +0 -14
  240. package/src/db/schema/1.0.0/consent-record.ts +0 -10
  241. package/src/db/schema/1.0.0/consent.ts +0 -20
  242. package/src/db/schema/1.0.0/domain.ts +0 -12
  243. package/src/db/schema/1.0.0/index.ts +0 -48
  244. package/src/db/schema/1.0.0/subject.ts +0 -11
  245. package/src/db/schema/2.0.0/audit-log.ts +0 -18
  246. package/src/db/schema/2.0.0/consent-policy.ts +0 -28
  247. package/src/db/schema/2.0.0/consent-purpose.ts +0 -12
  248. package/src/db/schema/2.0.0/consent.ts +0 -28
  249. package/src/db/schema/2.0.0/domain.ts +0 -12
  250. package/src/db/schema/2.0.0/index.ts +0 -47
  251. package/src/db/schema/2.0.0/subject.ts +0 -13
  252. package/src/db/schema/index.ts +0 -15
  253. package/src/db/tenant-scope.test.ts +0 -747
  254. package/src/db/tenant-scope.ts +0 -103
  255. package/src/define-config.ts +0 -5
  256. package/src/handlers/consent/check.handler.ts +0 -126
  257. package/src/handlers/init/geo.test.ts +0 -317
  258. package/src/handlers/init/geo.ts +0 -195
  259. package/src/handlers/init/index.test.ts +0 -205
  260. package/src/handlers/init/index.ts +0 -114
  261. package/src/handlers/init/translations.test.ts +0 -121
  262. package/src/handlers/init/translations.ts +0 -72
  263. package/src/handlers/status/status.handler.test.ts +0 -155
  264. package/src/handlers/status/status.handler.ts +0 -51
  265. package/src/handlers/subject/get.handler.ts +0 -92
  266. package/src/handlers/subject/list.handler.ts +0 -92
  267. package/src/handlers/subject/patch.handler.ts +0 -119
  268. package/src/handlers/subject/post.handler.test.ts +0 -294
  269. package/src/handlers/subject/post.handler.ts +0 -268
  270. package/src/handlers/utils/consent-enrichment.test.ts +0 -380
  271. package/src/handlers/utils/consent-enrichment.ts +0 -218
  272. package/src/init.test.ts +0 -126
  273. package/src/init.ts +0 -87
  274. package/src/middleware/auth/index.ts +0 -11
  275. package/src/middleware/auth/validate-api-key.test.ts +0 -86
  276. package/src/middleware/auth/validate-api-key.ts +0 -107
  277. package/src/middleware/cors/cors.test.ts +0 -135
  278. package/src/middleware/cors/cors.ts +0 -186
  279. package/src/middleware/cors/is-origin-trusted.test.ts +0 -164
  280. package/src/middleware/cors/is-origin-trusted.ts +0 -130
  281. package/src/middleware/cors/process-cors.ts +0 -91
  282. package/src/middleware/openapi/config.ts +0 -29
  283. package/src/middleware/openapi/handlers.ts +0 -34
  284. package/src/middleware/process-ip/index.test.ts +0 -195
  285. package/src/middleware/process-ip/index.ts +0 -199
  286. package/src/router.ts +0 -15
  287. package/src/routes/consent.ts +0 -52
  288. package/src/routes/init.ts +0 -105
  289. package/src/routes/status.ts +0 -46
  290. package/src/routes/subject.ts +0 -152
  291. package/src/types/api.ts +0 -48
  292. package/src/types/index.ts +0 -297
  293. package/src/utils/create-telemetry-options.test.ts +0 -302
  294. package/src/utils/create-telemetry-options.ts +0 -229
  295. package/src/utils/env.ts +0 -84
  296. package/src/utils/extract-error-message.ts +0 -21
  297. package/src/utils/instrumentation.test.ts +0 -185
  298. package/src/utils/instrumentation.ts +0 -196
  299. package/src/utils/logger.ts +0 -41
  300. package/src/utils/metrics.test.ts +0 -323
  301. package/src/utils/metrics.ts +0 -402
  302. package/src/utils/telemetry-pii.test.ts +0 -325
  303. package/src/version.ts +0 -2
  304. package/tsconfig.json +0 -11
  305. package/vitest.config.ts +0 -28
  306. /package/dist/{types.cjs → types/index.cjs} +0 -0
  307. /package/dist/{types.js → types/index.js} +0 -0
  308. /package/{src/db/adapters/drizzle.ts → dist-types/db/adapters/drizzle.d.ts} +0 -0
  309. /package/{src/db/adapters/index.ts → dist-types/db/adapters/index.d.ts} +0 -0
  310. /package/{src/db/adapters/kysely.ts → dist-types/db/adapters/kysely.d.ts} +0 -0
  311. /package/{src/db/adapters/mongo.ts → dist-types/db/adapters/mongo.d.ts} +0 -0
  312. /package/{src/db/adapters/prisma.ts → dist-types/db/adapters/prisma.d.ts} +0 -0
  313. /package/{src/db/adapters/typeorm.ts → dist-types/db/adapters/typeorm.d.ts} +0 -0
  314. /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
- });