@corgelabs/corge 0.0.1 → 1.0.1

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 (341) hide show
  1. package/dist/ai/index.d.ts +3 -0
  2. package/dist/ai/index.d.ts.map +1 -0
  3. package/dist/ai/index.js +3 -0
  4. package/dist/ai/index.js.map +1 -0
  5. package/dist/ai/schema.d.ts +83 -0
  6. package/dist/ai/schema.d.ts.map +1 -0
  7. package/dist/ai/schema.js +19 -0
  8. package/dist/ai/schema.js.map +1 -0
  9. package/dist/ai/service.d.ts +14 -0
  10. package/dist/ai/service.d.ts.map +1 -0
  11. package/dist/ai/service.js +266 -0
  12. package/dist/ai/service.js.map +1 -0
  13. package/dist/ai/utils.d.ts +10 -0
  14. package/dist/ai/utils.d.ts.map +1 -0
  15. package/dist/ai/utils.js +27 -0
  16. package/dist/ai/utils.js.map +1 -0
  17. package/dist/attachments/index.d.ts +4 -0
  18. package/dist/attachments/index.d.ts.map +1 -0
  19. package/dist/attachments/index.js +4 -0
  20. package/dist/attachments/index.js.map +1 -0
  21. package/dist/attachments/routes.d.ts +75 -0
  22. package/dist/attachments/routes.d.ts.map +1 -0
  23. package/dist/attachments/routes.js +18 -0
  24. package/dist/attachments/routes.js.map +1 -0
  25. package/dist/attachments/schema.d.ts +61 -0
  26. package/dist/attachments/schema.d.ts.map +1 -0
  27. package/dist/attachments/schema.js +30 -0
  28. package/dist/attachments/schema.js.map +1 -0
  29. package/dist/attachments/service.d.ts +27 -0
  30. package/dist/attachments/service.d.ts.map +1 -0
  31. package/dist/attachments/service.js +224 -0
  32. package/dist/attachments/service.js.map +1 -0
  33. package/dist/attachments/utils.d.ts +9 -0
  34. package/dist/attachments/utils.d.ts.map +1 -0
  35. package/dist/attachments/utils.js +80 -0
  36. package/dist/attachments/utils.js.map +1 -0
  37. package/dist/auth/index.d.ts +4 -0
  38. package/dist/auth/index.d.ts.map +1 -0
  39. package/dist/auth/index.js +4 -0
  40. package/dist/auth/index.js.map +1 -0
  41. package/dist/auth/oidc.d.ts +25 -0
  42. package/dist/auth/oidc.d.ts.map +1 -0
  43. package/dist/auth/oidc.js +298 -0
  44. package/dist/auth/oidc.js.map +1 -0
  45. package/dist/auth/recovery.d.ts +41 -0
  46. package/dist/auth/recovery.d.ts.map +1 -0
  47. package/dist/auth/recovery.js +250 -0
  48. package/dist/auth/recovery.js.map +1 -0
  49. package/dist/auth/routes.d.ts +226 -0
  50. package/dist/auth/routes.d.ts.map +1 -0
  51. package/dist/auth/routes.js +96 -0
  52. package/dist/auth/routes.js.map +1 -0
  53. package/dist/auth/schema.d.ts +158 -0
  54. package/dist/auth/schema.d.ts.map +1 -0
  55. package/dist/auth/schema.js +82 -0
  56. package/dist/auth/schema.js.map +1 -0
  57. package/dist/auth/service.d.ts +96 -0
  58. package/dist/auth/service.d.ts.map +1 -0
  59. package/dist/auth/service.js +129 -0
  60. package/dist/auth/service.js.map +1 -0
  61. package/dist/auth/session.d.ts +51 -0
  62. package/dist/auth/session.d.ts.map +1 -0
  63. package/dist/auth/session.js +344 -0
  64. package/dist/auth/session.js.map +1 -0
  65. package/dist/auth/utils.d.ts +17 -0
  66. package/dist/auth/utils.d.ts.map +1 -0
  67. package/dist/auth/utils.js +96 -0
  68. package/dist/auth/utils.js.map +1 -0
  69. package/dist/bin/corge.js +542 -0
  70. package/dist/chat/context.d.ts +10 -0
  71. package/dist/chat/context.d.ts.map +1 -0
  72. package/dist/chat/context.js +221 -0
  73. package/dist/chat/context.js.map +1 -0
  74. package/dist/chat/index.d.ts +4 -0
  75. package/dist/chat/index.d.ts.map +1 -0
  76. package/dist/chat/index.js +4 -0
  77. package/dist/chat/index.js.map +1 -0
  78. package/dist/chat/routes.d.ts +160 -0
  79. package/dist/chat/routes.d.ts.map +1 -0
  80. package/dist/chat/routes.js +210 -0
  81. package/dist/chat/routes.js.map +1 -0
  82. package/dist/chat/schema.d.ts +285 -0
  83. package/dist/chat/schema.d.ts.map +1 -0
  84. package/dist/chat/schema.js +74 -0
  85. package/dist/chat/schema.js.map +1 -0
  86. package/dist/chat/service.d.ts +64 -0
  87. package/dist/chat/service.d.ts.map +1 -0
  88. package/dist/chat/service.js +279 -0
  89. package/dist/chat/service.js.map +1 -0
  90. package/dist/chat/stream.d.ts +53 -0
  91. package/dist/chat/stream.d.ts.map +1 -0
  92. package/dist/chat/stream.js +339 -0
  93. package/dist/chat/stream.js.map +1 -0
  94. package/dist/chat/utils.d.ts +61 -0
  95. package/dist/chat/utils.d.ts.map +1 -0
  96. package/dist/chat/utils.js +285 -0
  97. package/dist/chat/utils.js.map +1 -0
  98. package/dist/cli/builder.d.ts +6 -0
  99. package/dist/cli/builder.d.ts.map +1 -0
  100. package/dist/cli/builder.js +91 -0
  101. package/dist/cli/builder.js.map +1 -0
  102. package/dist/cli/deployer.d.ts +2 -0
  103. package/dist/cli/deployer.d.ts.map +1 -0
  104. package/dist/cli/deployer.js +152 -0
  105. package/dist/cli/deployer.js.map +1 -0
  106. package/dist/cli/dev-server.d.ts +2 -0
  107. package/dist/cli/dev-server.d.ts.map +1 -0
  108. package/dist/cli/dev-server.js +70 -0
  109. package/dist/cli/dev-server.js.map +1 -0
  110. package/dist/cli/env.d.ts +4 -0
  111. package/dist/cli/env.d.ts.map +1 -0
  112. package/dist/cli/env.js +68 -0
  113. package/dist/cli/env.js.map +1 -0
  114. package/dist/cli/init.d.ts +6 -0
  115. package/dist/cli/init.d.ts.map +1 -0
  116. package/dist/cli/init.js +97 -0
  117. package/dist/cli/init.js.map +1 -0
  118. package/dist/config.d.ts +262 -0
  119. package/dist/config.d.ts.map +1 -0
  120. package/dist/config.js +212 -0
  121. package/dist/config.js.map +1 -0
  122. package/dist/db/index.d.ts +3 -0
  123. package/dist/db/index.d.ts.map +1 -0
  124. package/dist/db/index.js +3 -0
  125. package/dist/db/index.js.map +1 -0
  126. package/dist/db/schema.d.ts +410 -0
  127. package/dist/db/schema.d.ts.map +1 -0
  128. package/dist/db/schema.js +294 -0
  129. package/dist/db/schema.js.map +1 -0
  130. package/dist/db/service.d.ts +42 -0
  131. package/dist/db/service.d.ts.map +1 -0
  132. package/dist/db/service.js +372 -0
  133. package/dist/db/service.js.map +1 -0
  134. package/dist/db/utils.d.ts +25 -0
  135. package/dist/db/utils.d.ts.map +1 -0
  136. package/dist/db/utils.js +188 -0
  137. package/dist/db/utils.js.map +1 -0
  138. package/dist/emails/index.d.ts +4 -0
  139. package/dist/emails/index.d.ts.map +1 -0
  140. package/dist/emails/index.js +4 -0
  141. package/dist/emails/index.js.map +1 -0
  142. package/dist/emails/routes.d.ts +191 -0
  143. package/dist/emails/routes.d.ts.map +1 -0
  144. package/dist/emails/routes.js +39 -0
  145. package/dist/emails/routes.js.map +1 -0
  146. package/dist/emails/schema.d.ts +237 -0
  147. package/dist/emails/schema.d.ts.map +1 -0
  148. package/dist/emails/schema.js +98 -0
  149. package/dist/emails/schema.js.map +1 -0
  150. package/dist/emails/service.d.ts +22 -0
  151. package/dist/emails/service.d.ts.map +1 -0
  152. package/dist/emails/service.js +337 -0
  153. package/dist/emails/service.js.map +1 -0
  154. package/dist/emails/utils.d.ts +49 -0
  155. package/dist/emails/utils.d.ts.map +1 -0
  156. package/dist/emails/utils.js +341 -0
  157. package/dist/emails/utils.js.map +1 -0
  158. package/dist/files/index.d.ts +4 -0
  159. package/dist/files/index.d.ts.map +1 -0
  160. package/dist/files/index.js +4 -0
  161. package/dist/files/index.js.map +1 -0
  162. package/dist/files/routes.d.ts +72 -0
  163. package/dist/files/routes.d.ts.map +1 -0
  164. package/dist/files/routes.js +38 -0
  165. package/dist/files/routes.js.map +1 -0
  166. package/dist/files/schema.d.ts +57 -0
  167. package/dist/files/schema.d.ts.map +1 -0
  168. package/dist/files/schema.js +39 -0
  169. package/dist/files/schema.js.map +1 -0
  170. package/dist/files/service.d.ts +27 -0
  171. package/dist/files/service.d.ts.map +1 -0
  172. package/dist/files/service.js +175 -0
  173. package/dist/files/service.js.map +1 -0
  174. package/dist/files/utils.d.ts +3 -0
  175. package/dist/files/utils.d.ts.map +1 -0
  176. package/dist/files/utils.js +63 -0
  177. package/dist/files/utils.js.map +1 -0
  178. package/dist/index.d.ts +875 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +9919 -0
  181. package/dist/index.js.map +7 -0
  182. package/dist/payments/billing.d.ts +29 -0
  183. package/dist/payments/billing.d.ts.map +1 -0
  184. package/dist/payments/billing.js +378 -0
  185. package/dist/payments/billing.js.map +1 -0
  186. package/dist/payments/index.d.ts +4 -0
  187. package/dist/payments/index.d.ts.map +1 -0
  188. package/dist/payments/index.js +4 -0
  189. package/dist/payments/index.js.map +1 -0
  190. package/dist/payments/routes.d.ts +340 -0
  191. package/dist/payments/routes.d.ts.map +1 -0
  192. package/dist/payments/routes.js +56 -0
  193. package/dist/payments/routes.js.map +1 -0
  194. package/dist/payments/schema.d.ts +412 -0
  195. package/dist/payments/schema.d.ts.map +1 -0
  196. package/dist/payments/schema.js +260 -0
  197. package/dist/payments/schema.js.map +1 -0
  198. package/dist/payments/service.d.ts +31 -0
  199. package/dist/payments/service.d.ts.map +1 -0
  200. package/dist/payments/service.js +322 -0
  201. package/dist/payments/service.js.map +1 -0
  202. package/dist/payments/utils.d.ts +61 -0
  203. package/dist/payments/utils.d.ts.map +1 -0
  204. package/dist/payments/utils.js +369 -0
  205. package/dist/payments/utils.js.map +1 -0
  206. package/dist/router/guard.d.ts +19 -0
  207. package/dist/router/guard.d.ts.map +1 -0
  208. package/dist/router/guard.js +243 -0
  209. package/dist/router/guard.js.map +1 -0
  210. package/dist/router/index.d.ts +3 -0
  211. package/dist/router/index.d.ts.map +1 -0
  212. package/dist/router/index.js +3 -0
  213. package/dist/router/index.js.map +1 -0
  214. package/dist/router/schema.d.ts +103 -0
  215. package/dist/router/schema.d.ts.map +1 -0
  216. package/dist/router/schema.js +282 -0
  217. package/dist/router/schema.js.map +1 -0
  218. package/dist/router/service.d.ts +25 -0
  219. package/dist/router/service.d.ts.map +1 -0
  220. package/dist/router/service.js +339 -0
  221. package/dist/router/service.js.map +1 -0
  222. package/dist/tasks/index.d.ts +4 -0
  223. package/dist/tasks/index.d.ts.map +1 -0
  224. package/dist/tasks/index.js +4 -0
  225. package/dist/tasks/index.js.map +1 -0
  226. package/dist/tasks/routes.d.ts +54 -0
  227. package/dist/tasks/routes.d.ts.map +1 -0
  228. package/dist/tasks/routes.js +195 -0
  229. package/dist/tasks/routes.js.map +1 -0
  230. package/dist/tasks/schema.d.ts +261 -0
  231. package/dist/tasks/schema.d.ts.map +1 -0
  232. package/dist/tasks/schema.js +35 -0
  233. package/dist/tasks/schema.js.map +1 -0
  234. package/dist/tasks/service.d.ts +32 -0
  235. package/dist/tasks/service.d.ts.map +1 -0
  236. package/dist/tasks/service.js +201 -0
  237. package/dist/tasks/service.js.map +1 -0
  238. package/dist/tasks/steps.d.ts +18 -0
  239. package/dist/tasks/steps.d.ts.map +1 -0
  240. package/dist/tasks/steps.js +365 -0
  241. package/dist/tasks/steps.js.map +1 -0
  242. package/dist/tasks/utils.d.ts +75 -0
  243. package/dist/tasks/utils.d.ts.map +1 -0
  244. package/dist/tasks/utils.js +311 -0
  245. package/dist/tasks/utils.js.map +1 -0
  246. package/dist/tasks/watcher.d.ts +13 -0
  247. package/dist/tasks/watcher.d.ts.map +1 -0
  248. package/dist/tasks/watcher.js +255 -0
  249. package/dist/tasks/watcher.js.map +1 -0
  250. package/dist/tasks/worker.d.ts +28 -0
  251. package/dist/tasks/worker.d.ts.map +1 -0
  252. package/dist/tasks/worker.js +326 -0
  253. package/dist/tasks/worker.js.map +1 -0
  254. package/dist/utils/index.d.ts +3 -0
  255. package/dist/utils/index.d.ts.map +1 -0
  256. package/dist/utils/index.js +3 -0
  257. package/dist/utils/index.js.map +1 -0
  258. package/dist/utils/schema.d.ts +79 -0
  259. package/dist/utils/schema.d.ts.map +1 -0
  260. package/dist/utils/schema.js +41 -0
  261. package/dist/utils/schema.js.map +1 -0
  262. package/dist/utils/service.d.ts +32 -0
  263. package/dist/utils/service.d.ts.map +1 -0
  264. package/dist/utils/service.js +384 -0
  265. package/dist/utils/service.js.map +1 -0
  266. package/docs/SDK_SPEC.md +1645 -0
  267. package/infra/firestore.base.indexes.json +138 -0
  268. package/infra/firestore.base.rules +152 -0
  269. package/infra/storage.base.rules +8 -0
  270. package/package.json +58 -13
  271. package/src/ai/index.ts +2 -0
  272. package/src/ai/schema.ts +64 -0
  273. package/src/ai/service.ts +317 -0
  274. package/src/ai/utils.ts +38 -0
  275. package/src/attachments/index.ts +3 -0
  276. package/src/attachments/routes.ts +28 -0
  277. package/src/attachments/schema.ts +45 -0
  278. package/src/attachments/service.ts +315 -0
  279. package/src/attachments/utils.ts +115 -0
  280. package/src/auth/index.ts +3 -0
  281. package/src/auth/oidc.ts +419 -0
  282. package/src/auth/recovery.ts +365 -0
  283. package/src/auth/routes.ts +142 -0
  284. package/src/auth/schema.ts +181 -0
  285. package/src/auth/service.ts +183 -0
  286. package/src/auth/session.ts +414 -0
  287. package/src/auth/utils.ts +106 -0
  288. package/src/chat/context.ts +310 -0
  289. package/src/chat/index.ts +3 -0
  290. package/src/chat/routes.ts +264 -0
  291. package/src/chat/schema.ts +295 -0
  292. package/src/chat/service.ts +360 -0
  293. package/src/chat/stream.ts +481 -0
  294. package/src/chat/utils.ts +370 -0
  295. package/src/cli/builder.ts +93 -0
  296. package/src/cli/deployer.ts +184 -0
  297. package/src/cli/dev-server.ts +77 -0
  298. package/src/cli/env.ts +89 -0
  299. package/src/cli/init.ts +121 -0
  300. package/src/config.ts +447 -0
  301. package/src/db/index.ts +2 -0
  302. package/src/db/schema.ts +409 -0
  303. package/src/db/service.ts +477 -0
  304. package/src/db/utils.ts +250 -0
  305. package/src/emails/index.ts +3 -0
  306. package/src/emails/routes.ts +64 -0
  307. package/src/emails/schema.ts +239 -0
  308. package/src/emails/service.ts +488 -0
  309. package/src/emails/utils.ts +449 -0
  310. package/src/files/index.ts +3 -0
  311. package/src/files/routes.ts +44 -0
  312. package/src/files/schema.ts +76 -0
  313. package/src/files/service.ts +238 -0
  314. package/src/files/utils.ts +77 -0
  315. package/src/index.ts +425 -0
  316. package/src/payments/billing.ts +488 -0
  317. package/src/payments/index.ts +3 -0
  318. package/src/payments/routes.ts +92 -0
  319. package/src/payments/schema.ts +401 -0
  320. package/src/payments/service.ts +445 -0
  321. package/src/payments/utils.ts +472 -0
  322. package/src/router/guard.ts +319 -0
  323. package/src/router/index.ts +2 -0
  324. package/src/router/schema.ts +458 -0
  325. package/src/router/service.ts +453 -0
  326. package/src/tasks/index.ts +11 -0
  327. package/src/tasks/routes.ts +230 -0
  328. package/src/tasks/schema.ts +310 -0
  329. package/src/tasks/service.ts +254 -0
  330. package/src/tasks/steps.ts +472 -0
  331. package/src/tasks/utils.ts +445 -0
  332. package/src/tasks/watcher.ts +307 -0
  333. package/src/tasks/worker.ts +461 -0
  334. package/src/utils/index.ts +2 -0
  335. package/src/utils/schema.ts +69 -0
  336. package/src/utils/service.ts +461 -0
  337. package/tests/e2e.test.ts +2531 -0
  338. package/tsconfig.base.json +12 -0
  339. package/README.md +0 -2
  340. package/index.d.ts +0 -1
  341. package/index.js +0 -1
@@ -0,0 +1,477 @@
1
+ import { Agent } from 'node:https'
2
+ import { getApp, getApps, initializeApp } from 'firebase-admin/app'
3
+ import { FieldValue, getFirestore, type Firestore } from 'firebase-admin/firestore'
4
+ import { getStorage } from 'firebase-admin/storage'
5
+ import { CORE_CONFIG, type DbAction, type DbRole } from '../config'
6
+ import { Core } from '../index'
7
+ import type { StepRunner } from '../tasks'
8
+ import {
9
+ collectionRegistry,
10
+ getCollectionDef,
11
+ type CollectionName,
12
+ type DbCallContext,
13
+ type DbListOptions,
14
+ type DbListPage,
15
+ type EntryId,
16
+ type InsertData,
17
+ type PaginatedList,
18
+ type SelectModel,
19
+ type UpdateData
20
+ } from './schema'
21
+ import {
22
+ attachNextCursor,
23
+ buildScopedCollectionQuery,
24
+ drainQueryBatchDelete,
25
+ executeFilteredListQuery,
26
+ fromFirestoreDoc,
27
+ isCreateOwnScopeAllowed,
28
+ resolveAllowedScope,
29
+ resolveJunction,
30
+ runChunked,
31
+ stripImmutableUpdateFields,
32
+ wireCoreCollectionHooks
33
+ } from './utils'
34
+
35
+ type StorageBucket = ReturnType<ReturnType<typeof getStorage>['bucket']>
36
+
37
+ const dbs: [Firestore | undefined, Firestore | undefined] = [undefined, undefined]
38
+ const buckets: [StorageBucket | undefined, StorageBucket | undefined] = [undefined, undefined]
39
+
40
+ let cachedToken = ''
41
+ let cachedTokenExp = 0
42
+
43
+ export class DbService {
44
+ readonly #core: Core
45
+
46
+ constructor(coreClient: Core) {
47
+ this.#core = coreClient
48
+ wireCoreCollectionHooks()
49
+ }
50
+
51
+ #ensureFirebaseApp = (): void => {
52
+ if (!getApps().length) {
53
+ initializeApp({
54
+ projectId: CORE_CONFIG.gcpProject,
55
+ storageBucket: CORE_CONFIG.storageBuckets.prod,
56
+ serviceAccountId: CORE_CONFIG.serviceAccount,
57
+ httpAgent: new Agent({ keepAlive: true })
58
+ })
59
+ }
60
+ }
61
+
62
+ firestore = (isDev: boolean = this.#core.env.isDev): Firestore => {
63
+ this.#ensureFirebaseApp()
64
+ const idx = isDev ? 1 : 0
65
+ if (!dbs[idx]) {
66
+ const db = getFirestore(isDev ? CORE_CONFIG.firestoreDbs.dev : CORE_CONFIG.firestoreDbs.prod)
67
+ try {
68
+ db.settings({ ignoreUndefinedProperties: true })
69
+ } catch {}
70
+ dbs[idx] = db
71
+ }
72
+ return dbs[idx]!
73
+ }
74
+
75
+ bucket = (isDev: boolean = this.#core.env.isDev): StorageBucket => {
76
+ this.#ensureFirebaseApp()
77
+ const idx = isDev ? 1 : 0
78
+ if (!buckets[idx]) {
79
+ buckets[idx] = getStorage().bucket(isDev ? CORE_CONFIG.storageBuckets.dev : CORE_CONFIG.storageBuckets.prod)
80
+ }
81
+ return buckets[idx]!
82
+ }
83
+
84
+ #getGcpAccessToken = async (): Promise<string> => {
85
+ this.#ensureFirebaseApp()
86
+ if (cachedToken && cachedTokenExp > Date.now() + 60_000) return cachedToken
87
+ return this.#core.internals.resilience.singleflight(
88
+ 'gcp:token',
89
+ async () => {
90
+ const token = await getApp().options.credential?.getAccessToken()
91
+ if (!token?.access_token) throw new Error('Unable to acquire GCP ADC access token')
92
+ cachedToken = token.access_token
93
+ cachedTokenExp = Date.now() + (token.expires_in ? token.expires_in * 1000 : 3_000_000)
94
+ return cachedToken
95
+ },
96
+ { scope: 'global' }
97
+ )
98
+ }
99
+
100
+ #effectiveCtx = (ctx?: DbCallContext): DbCallContext => ({
101
+ userId: this.#core.userId,
102
+ userRole: this.#core.role,
103
+ ...ctx
104
+ })
105
+
106
+ #resolveRole = (ctx?: DbCallContext): DbRole =>
107
+ this.#core.internals.auth.resolveUserRole(ctx?.userId, ctx?.userRole)
108
+
109
+ #isEntryOwner = async (
110
+ collection: CollectionName,
111
+ doc: Record<string, any>,
112
+ userId: EntryId
113
+ ): Promise<boolean> => {
114
+ if (!userId) return false
115
+ if (userId === this.#core.config.auth.adminUserId) return true
116
+ const def = getCollectionDef(collection)
117
+ const rel = def.userRelation?.type ?? '1:N'
118
+ if (rel === 'global') return false
119
+ if (rel === '1:1') return doc[def.userRelation?.userField ?? 'id'] === userId
120
+ if (rel === '1:N') return doc[def.userRelation?.userField ?? 'userId'] === userId
121
+ if (rel === 'M:N' && def.userRelation?.junction) {
122
+ const { collection: jCol, userField, targetField } = resolveJunction(collection, def.userRelation.junction)
123
+ const jSnap = await this.firestore()
124
+ .collection(jCol)
125
+ .where(userField, '==', userId)
126
+ .where(targetField, '==', doc.id)
127
+ .limit(1)
128
+ .get()
129
+ return !jSnap.empty
130
+ }
131
+ const arr = doc[def.userRelation?.userIdsField ?? 'userIds']
132
+ return Array.isArray(arr) && arr.includes(userId)
133
+ }
134
+
135
+ #fetchRawEntry = async <T extends CollectionName>(
136
+ collection: T,
137
+ id: string
138
+ ): Promise<SelectModel<T> | undefined> => {
139
+ if (!id) return undefined
140
+ await this.#core.internals.resilience.acquireLimit('firestore')
141
+ const snap = await this.firestore().collection(collection).doc(id).get()
142
+ if (!snap.exists) return undefined
143
+ const doc = fromFirestoreDoc(collection, snap.id, snap.data() ?? {})
144
+ return doc.deleted ? undefined : doc
145
+ }
146
+
147
+ canAccess = async <N extends CollectionName>(
148
+ collection: N,
149
+ docOrId: string | Record<string, any> | undefined,
150
+ userId: EntryId = this.#core.userId,
151
+ action: DbAction = 'read',
152
+ userRole?: DbRole
153
+ ): Promise<boolean> => {
154
+ const role = this.#resolveRole({
155
+ userId,
156
+ userRole: userRole ?? (userId === this.#core.userId ? this.#core.role : undefined)
157
+ })
158
+ const scope = resolveAllowedScope(collection, role, action)
159
+ if (scope === 'none') return false
160
+ if (scope === 'any') {
161
+ if (!docOrId) return true
162
+ return typeof docOrId === 'string' ? Boolean(await this.#fetchRawEntry(collection, docOrId)) : true
163
+ }
164
+ if (!userId) return false
165
+ const doc =
166
+ typeof docOrId === 'string'
167
+ ? await this.#fetchRawEntry(collection, docOrId)
168
+ : (docOrId as Record<string, any> | undefined)
169
+ return doc ? this.#isEntryOwner(collection, doc, userId) : false
170
+ }
171
+
172
+ create = async <N extends CollectionName>(
173
+ collection: N,
174
+ data: InsertData<N>,
175
+ ctx?: DbCallContext
176
+ ): Promise<SelectModel<N>> => {
177
+ const effective = this.#effectiveCtx(ctx)
178
+ if (!effective.bypassRbac) {
179
+ const role = this.#resolveRole(effective)
180
+ const scope = resolveAllowedScope(collection, role, 'create')
181
+ if (scope === 'none') this.#core.errors.httpError(403, 'Forbidden', 'FORBIDDEN')
182
+ if (scope === 'own' && !isCreateOwnScopeAllowed(collection, effective.userId, data as Record<string, any>)) {
183
+ this.#core.errors.httpError(403, 'Forbidden', 'FORBIDDEN')
184
+ }
185
+ }
186
+ await this.#core.internals.resilience.acquireLimit('firestore')
187
+ const def = getCollectionDef(collection)
188
+ const rel = def.userRelation?.type ?? '1:N'
189
+ const userField = def.userRelation?.userField ?? (rel === '1:1' ? 'id' : 'userId')
190
+ const col = this.firestore().collection(collection)
191
+ const explicitId =
192
+ data.id ?? (rel === '1:1' && userField === 'id' ? (data as Record<string, any>).userId : undefined)
193
+ const docRef = explicitId ? col.doc(String(explicitId)) : col.doc()
194
+ const now = new Date()
195
+ const fullDoc = def.schema.parse({ deleted: false, ...data, id: docRef.id, createdAt: now, updatedAt: now })
196
+ await docRef.set(fullDoc)
197
+ return fullDoc as SelectModel<N>
198
+ }
199
+
200
+ read = async <N extends CollectionName>(
201
+ collection: N,
202
+ id: EntryId,
203
+ includeDeleted = false,
204
+ ctx?: DbCallContext
205
+ ): Promise<SelectModel<N> | undefined> => {
206
+ if (!id) return undefined
207
+ await this.#core.internals.resilience.acquireLimit('firestore')
208
+ const snap = await this.firestore().collection(collection).doc(id).get()
209
+ if (!snap.exists) return undefined
210
+ const doc = fromFirestoreDoc(collection, snap.id, snap.data() ?? {})
211
+ if (!includeDeleted && doc.deleted) return undefined
212
+ const effective = this.#effectiveCtx(ctx)
213
+ if (!effective.bypassRbac) {
214
+ const role = this.#resolveRole(effective)
215
+ const scope = resolveAllowedScope(collection, role, 'read')
216
+ if (scope === 'none') this.#core.errors.httpError(403, 'Forbidden', 'FORBIDDEN')
217
+ if (scope === 'own') {
218
+ if (!effective.userId || !(await this.#isEntryOwner(collection, doc, effective.userId))) {
219
+ this.#core.errors.httpError(403, 'Forbidden', 'FORBIDDEN')
220
+ }
221
+ }
222
+ }
223
+ return doc
224
+ }
225
+
226
+ #batchReadByIds = async <T extends CollectionName>(
227
+ collection: T,
228
+ ids: string[],
229
+ includeDeleted = false,
230
+ hasSelect = false
231
+ ): Promise<SelectModel<T>[]> => {
232
+ const uniqueIds = [...new Set(ids.filter(Boolean))]
233
+ if (!uniqueIds.length) return []
234
+ const byId = new Map<string, SelectModel<T>>()
235
+ const db = this.firestore()
236
+ for (let i = 0; i < uniqueIds.length; i += 100) {
237
+ await this.#core.internals.resilience.acquireLimit('firestore')
238
+ const snaps = await db.getAll(...uniqueIds.slice(i, i + 100).map((id) => db.collection(collection).doc(id)))
239
+ for (const s of snaps) {
240
+ if (!s.exists) continue
241
+ const doc = fromFirestoreDoc(collection, s.id, s.data() ?? {}, hasSelect)
242
+ if (includeDeleted || !doc.deleted) byId.set(s.id, doc)
243
+ }
244
+ }
245
+ return ids.map((id) => byId.get(id)).filter((d): d is SelectModel<T> => Boolean(d))
246
+ }
247
+
248
+ list = async <N extends CollectionName>(
249
+ collection: N,
250
+ opts?: DbListOptions<SelectModel<N>>,
251
+ ctx?: DbCallContext
252
+ ): Promise<PaginatedList<SelectModel<N>>> => {
253
+ const effective = this.#effectiveCtx(ctx)
254
+ const bypass = Boolean(opts?.bypassRbac || effective.bypassRbac)
255
+ let scopedUserId = opts?.userId
256
+ if (!bypass) {
257
+ const role = this.#resolveRole(effective)
258
+ const scope = resolveAllowedScope(collection, role, 'read')
259
+ if (scope === 'none') this.#core.errors.httpError(403, 'Forbidden', 'FORBIDDEN')
260
+ if (scope === 'own') {
261
+ if (!effective.userId) this.#core.errors.httpError(403, 'Forbidden', 'FORBIDDEN')
262
+ if (scopedUserId && scopedUserId !== effective.userId) this.#core.errors.httpError(403, 'Forbidden', 'FORBIDDEN')
263
+ scopedUserId = effective.userId
264
+ }
265
+ }
266
+
267
+ const def = getCollectionDef(collection)
268
+ const db = this.firestore()
269
+ const rel = def.userRelation?.type ?? '1:N'
270
+ const maxDocs = opts?.limit ?? this.#core.config.limits.defaultListLimit
271
+ const hasSelect = Boolean(opts?.select?.length)
272
+
273
+ if (opts?.ids) {
274
+ const docs = await this.#batchReadByIds(collection, opts.ids, opts.includeDeleted, hasSelect)
275
+ if (!scopedUserId || rel === 'global') return attachNextCursor(docs.slice(0, maxDocs), maxDocs)
276
+ const ownership = await Promise.all(docs.map((doc) => this.#isEntryOwner(collection, doc, scopedUserId)))
277
+ return attachNextCursor(docs.filter((_, i) => ownership[i]).slice(0, maxDocs), maxDocs)
278
+ }
279
+
280
+ await this.#core.internals.resilience.acquireLimit('firestore')
281
+
282
+ if (scopedUserId) {
283
+ if (rel === '1:1' && (def.userRelation?.userField ?? 'id') === 'id') {
284
+ const single = await this.read(collection, scopedUserId, opts?.includeDeleted, { bypassRbac: true })
285
+ return attachNextCursor(single ? [single] : [], maxDocs)
286
+ }
287
+ if (rel === 'M:N' && def.userRelation?.junction) {
288
+ const { collection: jCol, userField, targetField } = resolveJunction(collection, def.userRelation.junction)
289
+ const jSnap = await db.collection(jCol).where(userField, '==', scopedUserId).limit(maxDocs).get()
290
+ const targetIds = jSnap.docs.map((d) => String(d.data()[targetField] ?? '')).filter(Boolean)
291
+ const docs = await this.#batchReadByIds(collection, targetIds, opts?.includeDeleted, hasSelect)
292
+ return attachNextCursor(docs, maxDocs)
293
+ }
294
+ }
295
+
296
+ const baseQuery = buildScopedCollectionQuery(
297
+ db.collection(collection),
298
+ collection,
299
+ scopedUserId,
300
+ opts?.where as Record<string, unknown> | undefined,
301
+ opts?.select
302
+ )
303
+ const docs = await executeFilteredListQuery(
304
+ db,
305
+ collection,
306
+ baseQuery,
307
+ opts,
308
+ maxDocs,
309
+ hasSelect,
310
+ this.#core.config.limits.defaultListLimit
311
+ )
312
+ return attachNextCursor(docs.slice(0, maxDocs), maxDocs)
313
+ }
314
+
315
+ listPage = async <N extends CollectionName>(
316
+ collection: N,
317
+ opts?: DbListOptions<SelectModel<N>>,
318
+ ctx?: DbCallContext
319
+ ): Promise<DbListPage<SelectModel<N>>> => {
320
+ const items = await this.list(collection, opts, ctx)
321
+ return { items, nextCursor: items.nextCursor ?? null }
322
+ }
323
+
324
+ update = async <N extends CollectionName>(
325
+ collection: N,
326
+ id: EntryId,
327
+ data: UpdateData<N>,
328
+ ctx?: DbCallContext
329
+ ): Promise<{ id: EntryId; updated: true }> => {
330
+ const effective = this.#effectiveCtx(ctx)
331
+ if (!effective.bypassRbac) {
332
+ const allowed = await this.canAccess(collection, id, effective.userId ?? '', 'write', effective.userRole)
333
+ if (!allowed) this.#core.errors.httpError(403, 'Forbidden', 'FORBIDDEN')
334
+ }
335
+ const cleanPatch = stripImmutableUpdateFields(collection, data, Boolean(effective.bypassRbac))
336
+ await this.#core.internals.resilience.acquireLimit('firestore')
337
+ await this.firestore()
338
+ .collection(collection)
339
+ .doc(id)
340
+ .update({ ...cleanPatch, updatedAt: new Date() })
341
+ return { id, updated: true }
342
+ }
343
+
344
+ delete = async <N extends CollectionName>(
345
+ collection: N,
346
+ id: EntryId,
347
+ ctx?: DbCallContext
348
+ ): Promise<{ id: EntryId; deleted: true }> => {
349
+ const existing = await this.read(collection, id, false, { bypassRbac: true })
350
+ if (!existing) throw new Error(`Not found: ${collection}/${id}`)
351
+ const effective = this.#effectiveCtx(ctx)
352
+ if (!effective.bypassRbac) {
353
+ const allowed = await this.canAccess(collection, existing, effective.userId ?? '', 'delete', effective.userRole)
354
+ if (!allowed) this.#core.errors.httpError(403, 'Forbidden', 'FORBIDDEN')
355
+ }
356
+ await this.update(collection, id, { deleted: true } as UpdateData<N>, { bypassRbac: true })
357
+ return { id, deleted: true }
358
+ }
359
+
360
+ hardDelete = async <N extends CollectionName>(
361
+ collection: N,
362
+ id: EntryId,
363
+ ctx?: DbCallContext
364
+ ): Promise<{ id: EntryId; deleted: true; hard: true }> => {
365
+ const def = getCollectionDef(collection)
366
+ const effective = this.#effectiveCtx(ctx)
367
+ const existing =
368
+ def.onHardDelete || !effective.bypassRbac
369
+ ? await this.read(collection, id, true, { bypassRbac: true })
370
+ : undefined
371
+ if (!effective.bypassRbac) {
372
+ if (!existing) throw new Error(`Not found: ${collection}/${id}`)
373
+ const allowed = await this.canAccess(collection, existing, effective.userId ?? '', 'delete', effective.userRole)
374
+ if (!allowed) this.#core.errors.httpError(403, 'Forbidden', 'FORBIDDEN')
375
+ }
376
+ await this.#core.internals.resilience.acquireLimit('firestore')
377
+ await this.firestore().collection(collection).doc(id).delete()
378
+ if (def.onHardDelete && (existing || collection === 'chat')) {
379
+ const scopedCore =
380
+ effective.core ??
381
+ new Core({
382
+ env: this.#core.env,
383
+ userId: effective.userId ?? this.#core.config.auth.adminUserId,
384
+ role: 'admin'
385
+ })
386
+ const targetDoc = existing ?? ({ id } as SelectModel<N>)
387
+ await def.onHardDelete(targetDoc, scopedCore).catch(() => {})
388
+ }
389
+ return { id, deleted: true, hard: true }
390
+ }
391
+
392
+ #eraseOwnedEntries = async (name: string, userId: EntryId): Promise<number> => {
393
+ let total = 0
394
+ while (true) {
395
+ const docs = await this.list(name, { userId, includeDeleted: true, limit: 500, bypassRbac: true })
396
+ if (!docs.length) break
397
+ await runChunked(docs, (d) => this.hardDelete(name, d.id, { bypassRbac: true }).catch(() => {}), 25)
398
+ total += docs.length
399
+ if (docs.length < 500) break
400
+ }
401
+ return total
402
+ }
403
+
404
+ #cascadeEraseUserCollections = async (
405
+ userId: EntryId,
406
+ step: StepRunner = this.#core.step
407
+ ): Promise<{ erasedFiles: number; erasedChats: number; erasedCollections: Record<string, number> }> => {
408
+ const erasedFiles = await step('erase-files', () => this.#eraseOwnedEntries('file', userId))
409
+ const erasedChats = await step('erase-chats', async () => {
410
+ const count = await this.#eraseOwnedEntries('chat', userId)
411
+ const db = this.firestore()
412
+ await drainQueryBatchDelete(db, db.collectionGroup('messages').where('userId', '==', userId)).catch(() => 0)
413
+ return count
414
+ })
415
+
416
+ const erasedCollections: Record<string, number> = {}
417
+ const db = this.firestore()
418
+
419
+ for (const [name, def] of collectionRegistry) {
420
+ if (name === 'file' || name === 'chat') continue
421
+ const rel = def.userRelation?.type ?? '1:N'
422
+ if (rel === 'global') continue
423
+
424
+ erasedCollections[name] = await step(`erase-${name}`, async () => {
425
+ if (rel === '1:1' || rel === '1:N') return this.#eraseOwnedEntries(name, userId)
426
+ if (rel === 'M:N' && def.userRelation?.junction) {
427
+ const { collection: jCol, userField } = resolveJunction(name, def.userRelation.junction)
428
+ const jSnap = await db.collection(jCol).where(userField, '==', userId).get()
429
+ await runChunked(jSnap.docs, (d) => d.ref.delete().catch(() => {}), 25)
430
+ return jSnap.size
431
+ }
432
+ const userIdsField = def.userRelation?.userIdsField ?? 'userIds'
433
+ const docs = await this.list(name, { userId, includeDeleted: true, limit: 500, bypassRbac: true })
434
+ await runChunked(docs, async (d) => {
435
+ const currentIds: string[] = Array.isArray(d[userIdsField]) ? d[userIdsField] : []
436
+ if (def.userRelation?.deleteWhenEmpty && currentIds.filter((u) => u !== userId).length === 0) {
437
+ await this.hardDelete(name, d.id, { bypassRbac: true }).catch(() => {})
438
+ } else {
439
+ await db.collection(name).doc(d.id).update({ [userIdsField]: FieldValue.arrayRemove(userId), updatedAt: new Date() }).catch(() => {})
440
+ }
441
+ })
442
+ return docs.length
443
+ })
444
+ }
445
+
446
+ await step('erase-tasks', async () => {
447
+ const snap = await db.collection('_tasks').where('userId', '==', userId).get()
448
+ const bucket = this.bucket()
449
+ const docsToDelete = snap.docs.filter(
450
+ (d) => d.data().workflowName !== 'erase-user' || d.data().status !== 'running'
451
+ )
452
+ await Promise.all([
453
+ runChunked(
454
+ docsToDelete,
455
+ (d) =>
456
+ Promise.all([
457
+ d.ref.delete().catch(() => {}),
458
+ bucket.deleteFiles({ prefix: `_tasks/${d.id}/` }).catch(() => {})
459
+ ]),
460
+ 25
461
+ ),
462
+ drainQueryBatchDelete(db, db.collection('_idempotency').where('userId', '==', userId)).catch(() => 0)
463
+ ])
464
+ return snap.size
465
+ })
466
+
467
+ return { erasedFiles, erasedChats, erasedCollections }
468
+ }
469
+
470
+ get internals() {
471
+ return {
472
+ ensureFirebaseApp: this.#ensureFirebaseApp,
473
+ getGcpAccessToken: this.#getGcpAccessToken,
474
+ cascadeEraseUserCollections: this.#cascadeEraseUserCollections
475
+ }
476
+ }
477
+ }