appflare 0.0.28 → 0.1.0

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 (141) hide show
  1. package/cli/commands/index.ts +140 -0
  2. package/cli/generate.ts +149 -0
  3. package/cli/index.ts +56 -447
  4. package/cli/load-config.ts +182 -0
  5. package/cli/schema-compiler.ts +657 -0
  6. package/cli/templates/auth/README.md +156 -0
  7. package/cli/templates/auth/config.ts +61 -0
  8. package/cli/templates/auth/route-config.ts +18 -0
  9. package/cli/templates/auth/route-handler.ts +18 -0
  10. package/cli/templates/auth/route-request-utils.ts +55 -0
  11. package/cli/templates/auth/route.ts +14 -0
  12. package/cli/templates/core/README.md +266 -0
  13. package/cli/templates/core/app-creation.ts +19 -0
  14. package/cli/templates/core/client/appflare.ts +37 -0
  15. package/cli/templates/core/client/index.ts +6 -0
  16. package/cli/templates/core/client/storage.ts +100 -0
  17. package/cli/templates/core/client/types.ts +54 -0
  18. package/cli/templates/core/client-modules/appflare.ts +112 -0
  19. package/cli/templates/core/client-modules/handlers/index.ts +740 -0
  20. package/cli/templates/core/client-modules/handlers.ts +1 -0
  21. package/cli/templates/core/client-modules/index.ts +7 -0
  22. package/cli/templates/core/client-modules/storage.ts +180 -0
  23. package/cli/templates/core/client-modules/types.ts +145 -0
  24. package/cli/templates/core/client.ts +39 -0
  25. package/cli/templates/core/drizzle.ts +15 -0
  26. package/cli/templates/core/export.ts +14 -0
  27. package/cli/templates/core/handlers-route.ts +23 -0
  28. package/cli/templates/core/handlers.ts +1 -0
  29. package/cli/templates/core/imports.ts +8 -0
  30. package/cli/templates/core/server.ts +38 -0
  31. package/cli/templates/core/types.ts +6 -0
  32. package/cli/templates/core/wrangler.ts +109 -0
  33. package/cli/templates/handlers/README.md +265 -0
  34. package/cli/templates/handlers/auth.ts +36 -0
  35. package/cli/templates/handlers/execution.ts +39 -0
  36. package/cli/templates/handlers/generators/context/context-creation.ts +80 -0
  37. package/cli/templates/handlers/generators/context/error-helpers.ts +11 -0
  38. package/cli/templates/handlers/generators/context/scheduler.ts +24 -0
  39. package/cli/templates/handlers/generators/context/storage-api.ts +112 -0
  40. package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -0
  41. package/cli/templates/handlers/generators/context/types.ts +18 -0
  42. package/cli/templates/handlers/generators/context.ts +43 -0
  43. package/cli/templates/handlers/generators/execution.ts +15 -0
  44. package/cli/templates/handlers/generators/handlers.ts +13 -0
  45. package/cli/templates/handlers/index.ts +43 -0
  46. package/cli/templates/handlers/operations.ts +116 -0
  47. package/cli/templates/handlers/registration.ts +1114 -0
  48. package/cli/templates/handlers/types.ts +960 -0
  49. package/cli/templates/handlers/utils.ts +48 -0
  50. package/cli/types.ts +108 -0
  51. package/cli/utils/handler-discovery.ts +366 -0
  52. package/cli/utils/json-utils.ts +24 -0
  53. package/cli/utils/path-utils.ts +19 -0
  54. package/cli/utils/schema-discovery.ts +390 -0
  55. package/index.ts +27 -4
  56. package/package.json +23 -20
  57. package/react/index.ts +5 -3
  58. package/react/use-infinite-query.ts +190 -0
  59. package/react/use-mutation.ts +54 -0
  60. package/react/use-query.ts +158 -0
  61. package/schema.ts +262 -0
  62. package/tsconfig.json +2 -4
  63. package/cli/README.md +0 -108
  64. package/cli/core/build.ts +0 -187
  65. package/cli/core/config.ts +0 -92
  66. package/cli/core/discover-handlers.ts +0 -143
  67. package/cli/core/handlers.ts +0 -7
  68. package/cli/core/index.ts +0 -205
  69. package/cli/generators/generate-api-client/client.ts +0 -163
  70. package/cli/generators/generate-api-client/extract-configuration.ts +0 -121
  71. package/cli/generators/generate-api-client/index.ts +0 -973
  72. package/cli/generators/generate-api-client/types.ts +0 -164
  73. package/cli/generators/generate-api-client/utils.ts +0 -22
  74. package/cli/generators/generate-api-client.ts +0 -1
  75. package/cli/generators/generate-cloudflare-worker/helpers.ts +0 -24
  76. package/cli/generators/generate-cloudflare-worker/index.ts +0 -2
  77. package/cli/generators/generate-cloudflare-worker/worker.ts +0 -148
  78. package/cli/generators/generate-cloudflare-worker/wrangler.ts +0 -108
  79. package/cli/generators/generate-cloudflare-worker.ts +0 -4
  80. package/cli/generators/generate-cron-handlers/cron-handlers-block.ts +0 -2
  81. package/cli/generators/generate-cron-handlers/handler-entries.ts +0 -29
  82. package/cli/generators/generate-cron-handlers/index.ts +0 -61
  83. package/cli/generators/generate-cron-handlers/runtime-block.ts +0 -49
  84. package/cli/generators/generate-cron-handlers/type-helpers-block.ts +0 -60
  85. package/cli/generators/generate-db-handlers/index.ts +0 -33
  86. package/cli/generators/generate-db-handlers/prepare.ts +0 -24
  87. package/cli/generators/generate-db-handlers/templates.ts +0 -189
  88. package/cli/generators/generate-db-handlers.ts +0 -1
  89. package/cli/generators/generate-hono-server/auth.ts +0 -97
  90. package/cli/generators/generate-hono-server/imports.ts +0 -55
  91. package/cli/generators/generate-hono-server/index.ts +0 -52
  92. package/cli/generators/generate-hono-server/routes.ts +0 -115
  93. package/cli/generators/generate-hono-server/template.ts +0 -371
  94. package/cli/generators/generate-hono-server.ts +0 -1
  95. package/cli/generators/generate-scheduler-handlers/constants.ts +0 -8
  96. package/cli/generators/generate-scheduler-handlers/handler-entries.ts +0 -22
  97. package/cli/generators/generate-scheduler-handlers/index.ts +0 -51
  98. package/cli/generators/generate-scheduler-handlers/runtime-block.ts +0 -68
  99. package/cli/generators/generate-scheduler-handlers/scheduler-handlers-block.ts +0 -2
  100. package/cli/generators/generate-scheduler-handlers/type-helpers-block.ts +0 -68
  101. package/cli/generators/generate-scheduler-handlers.ts +0 -1
  102. package/cli/generators/generate-websocket-durable-object/auth.ts +0 -30
  103. package/cli/generators/generate-websocket-durable-object/imports.ts +0 -55
  104. package/cli/generators/generate-websocket-durable-object/index.ts +0 -41
  105. package/cli/generators/generate-websocket-durable-object/query-handlers.ts +0 -18
  106. package/cli/generators/generate-websocket-durable-object/template.ts +0 -714
  107. package/cli/generators/generate-websocket-durable-object.ts +0 -1
  108. package/cli/schema/schema-static-types.ts +0 -702
  109. package/cli/schema/schema.ts +0 -151
  110. package/cli/utils/tsc.ts +0 -54
  111. package/cli/utils/utils.ts +0 -190
  112. package/cli/utils/zod-utils.ts +0 -121
  113. package/lib/README.md +0 -50
  114. package/lib/db.ts +0 -19
  115. package/lib/location.ts +0 -110
  116. package/lib/values.ts +0 -27
  117. package/react/README.md +0 -67
  118. package/react/hooks/useMutation.ts +0 -89
  119. package/react/hooks/usePaginatedQuery.ts +0 -213
  120. package/react/hooks/useQuery.ts +0 -106
  121. package/react/shared/queryShared.ts +0 -174
  122. package/server/README.md +0 -218
  123. package/server/auth.ts +0 -107
  124. package/server/database/builders.ts +0 -83
  125. package/server/database/context.ts +0 -327
  126. package/server/database/populate.ts +0 -234
  127. package/server/database/query-builder.ts +0 -161
  128. package/server/database/query-utils.ts +0 -25
  129. package/server/db.ts +0 -2
  130. package/server/storage/auth.ts +0 -16
  131. package/server/storage/bucket.ts +0 -22
  132. package/server/storage/context.ts +0 -34
  133. package/server/storage/index.ts +0 -38
  134. package/server/storage/operations.ts +0 -149
  135. package/server/storage/route-handler.ts +0 -60
  136. package/server/storage/types.ts +0 -55
  137. package/server/storage/utils.ts +0 -47
  138. package/server/storage.ts +0 -6
  139. package/server/types/schema-refs.ts +0 -66
  140. package/server/types/types.ts +0 -633
  141. package/server/utils/id-utils.ts +0 -230
@@ -1,327 +0,0 @@
1
- import type { Collection, Document } from "mongodb";
2
- import { ObjectId } from "mongodb";
3
- import { createPatchBuilder, createUpdateBuilder } from "./builders";
4
- import { createQueryBuilder } from "./query-builder";
5
- import {
6
- isIdValue,
7
- normalizeIdFilter,
8
- normalizeRefFields,
9
- toMongoFilter,
10
- } from "../utils/id-utils";
11
- import { buildSchemaRefMap } from "../types/schema-refs";
12
- import type {
13
- CreateMongoDbContextOptions,
14
- MongoDbCoreContext,
15
- MongoDbContext,
16
- MongoDbQuery,
17
- AppflareTableClient,
18
- TableDocBase,
19
- } from "../types/types";
20
-
21
- export function createMongoDbContext<
22
- TTableNames extends string,
23
- TTableDocMap extends Record<TTableNames, TableDocBase>,
24
- >(
25
- options: CreateMongoDbContextOptions<TTableNames>
26
- ): MongoDbContext<TTableNames, TTableDocMap> {
27
- const collectionName = options.collectionName ?? ((t) => t);
28
- const collections = new Map<string, Collection<Document>>();
29
- const refs = buildSchemaRefMap(options.schema);
30
- const tableNames = Object.keys(options.schema) as TTableNames[];
31
-
32
- const getCollection = (table: string): Collection<Document> => {
33
- const key = collectionName(table as any);
34
- const existing = collections.get(key);
35
- if (existing) return existing;
36
- const created = options.db.collection(key);
37
- collections.set(key, created);
38
- return created;
39
- };
40
-
41
- const core: MongoDbCoreContext<TTableNames, TTableDocMap> = {
42
- query: (table) =>
43
- createQueryBuilder<TTableNames, TTableDocMap, any>({
44
- table: table as any,
45
- getCollection,
46
- refs,
47
- }),
48
- insert: async (table, value) => {
49
- const coll = getCollection(table as string);
50
- const objectId = new ObjectId();
51
- const normalized = normalizeRefFields(
52
- table as string,
53
- value as Record<string, unknown>,
54
- refs
55
- );
56
- const doc = {
57
- ...normalized,
58
- _id: objectId,
59
- _creationTime: Date.now(),
60
- } as unknown as Document;
61
-
62
- await coll.insertOne(doc);
63
- return objectId.toHexString() as any;
64
- },
65
- update: ((table: any, where?: any, partial?: any) => {
66
- const coll = getCollection(table as string);
67
- const filter = normalizeIdFilter(toMongoFilter(where));
68
- const update = {
69
- $set: normalizeRefFields(table as string, partial as any, refs) as any,
70
- };
71
- if (isIdValue(where)) {
72
- return coll.updateOne(filter, update).then(() => {});
73
- }
74
- return coll.updateMany(filter, update).then(() => {});
75
- }) as any,
76
- patch: ((table: any, where?: any, partial?: any) => {
77
- const coll = getCollection(table as string);
78
- const filter = normalizeIdFilter(toMongoFilter(where));
79
- const update = {
80
- $set: normalizeRefFields(table as string, partial as any, refs) as any,
81
- };
82
- if (isIdValue(where)) {
83
- return coll.updateOne(filter, update).then(() => {});
84
- }
85
- return coll.updateMany(filter, update).then(() => {});
86
- }) as any,
87
- delete: ((table: any, where?: any) => {
88
- const coll = getCollection(table as string);
89
- const filter = normalizeIdFilter(toMongoFilter(where));
90
- if (isIdValue(where)) {
91
- return coll.deleteOne(filter).then(() => {});
92
- }
93
- return coll.deleteMany(filter).then(() => {});
94
- }) as any,
95
- };
96
-
97
- const ctx = {} as MongoDbContext<TTableNames, TTableDocMap>;
98
-
99
- for (const table of tableNames) {
100
- (ctx as any)[table] = createAppflareTableClient({
101
- table: table as TTableNames,
102
- core,
103
- refs,
104
- getCollection,
105
- });
106
- }
107
-
108
- return ctx;
109
- }
110
-
111
- function toKeyList(arg: unknown): string[] | undefined {
112
- if (!arg) return undefined;
113
- if (typeof arg === "string" || typeof arg === "number") return [String(arg)];
114
- if (Array.isArray(arg)) return arg.map((k) => String(k));
115
- if (typeof arg === "object") {
116
- return Object.entries(arg as Record<string, unknown>)
117
- .filter(([, v]) => Boolean(v))
118
- .map(([k]) => k);
119
- }
120
- return undefined;
121
- }
122
-
123
- function createAppflareTableClient<
124
- TTableNames extends string,
125
- TTableDocMap extends Record<TTableNames, TableDocBase>,
126
- TableName extends TTableNames,
127
- >(params: {
128
- table: TableName;
129
- core: MongoDbCoreContext<TTableNames, TTableDocMap>;
130
- refs: ReturnType<typeof buildSchemaRefMap>;
131
- getCollection: (table: string) => Collection<Document>;
132
- }): AppflareTableClient<TableName, TTableDocMap> {
133
- const selectKeys = (select: unknown) => toKeyList(select);
134
-
135
- const toArray = <T>(value: T | T[] | undefined): T[] => {
136
- if (value === undefined) return [];
137
- return Array.isArray(value) ? value : [value];
138
- };
139
-
140
- const stringifyAggregateValue = (value: unknown): unknown => {
141
- if (value instanceof ObjectId) return value.toHexString();
142
- if (Array.isArray(value)) return value.map(stringifyAggregateValue);
143
- if (value && typeof value === "object") {
144
- const out: Record<string, unknown> = {};
145
- for (const [key, val] of Object.entries(
146
- value as Record<string, unknown>
147
- )) {
148
- out[key] = stringifyAggregateValue(val);
149
- }
150
- return out;
151
- }
152
- return value;
153
- };
154
-
155
- const buildQuery = (args?: {
156
- where?: any;
157
- orderBy?: any;
158
- skip?: number;
159
- take?: number;
160
- select?: unknown;
161
- include?: unknown;
162
- }) => {
163
- let q: MongoDbQuery<TableName, TTableDocMap, any> = params.core.query(
164
- params.table as any
165
- ) as any;
166
- if (args?.where) q = q.where(args.where as any);
167
- if (args?.orderBy) q = q.sort(args.orderBy as any);
168
- if (args?.skip !== undefined) q = q.offset(args.skip);
169
- if (args?.take !== undefined) q = q.limit(args.take);
170
- const s = selectKeys(args?.select);
171
- if (s?.length) q = q.select(s as any);
172
- if (args?.include) q = q.populate(args.include as any);
173
- return q;
174
- };
175
-
176
- const fetchOne = async (args: {
177
- where: any;
178
- select?: unknown;
179
- include?: unknown;
180
- }) => {
181
- const q = buildQuery({ ...args, take: 1 });
182
- return q.findOne();
183
- };
184
-
185
- return {
186
- findMany: async (args) => buildQuery(args as any).find() as any,
187
- findFirst: async (args) =>
188
- buildQuery({
189
- ...(args as any),
190
- take: (args as any)?.take ?? 1,
191
- }).findOne() as any,
192
- findUnique: async (args) => {
193
- if (!args?.where) throw new Error("findUnique requires a where clause");
194
- return fetchOne(args as any) as any;
195
- },
196
- create: async (args) => {
197
- const id = await params.core.insert(
198
- params.table as any,
199
- args.data as any
200
- );
201
- const created = await fetchOne({
202
- where: { _id: id } as any,
203
- select: (args as any)?.select,
204
- include: (args as any)?.include,
205
- });
206
- return (created ?? {
207
- _id: id,
208
- _creationTime: Date.now(),
209
- ...args.data,
210
- }) as any;
211
- },
212
- update: async (args) => {
213
- await params.core.update(
214
- params.table as any,
215
- args.where as any,
216
- args.data as any
217
- );
218
- return fetchOne({
219
- where: args.where as any,
220
- select: (args as any)?.select,
221
- include: (args as any)?.include,
222
- }) as any;
223
- },
224
- updateMany: async (args) => {
225
- const coll = params.getCollection(params.table as string);
226
- const filter = normalizeIdFilter(toMongoFilter(args.where ?? {}));
227
- const normalized = normalizeRefFields(
228
- params.table as string,
229
- args.data as any,
230
- params.refs
231
- );
232
- const result = await coll.updateMany(filter as any, {
233
- $set: normalized as any,
234
- });
235
- return { count: result.modifiedCount ?? 0 };
236
- },
237
- delete: async (args) => {
238
- const existing = await fetchOne({
239
- where: args.where as any,
240
- select: (args as any)?.select,
241
- include: (args as any)?.include,
242
- });
243
- await params.core.delete(params.table as any, args.where as any);
244
- return existing as any;
245
- },
246
- deleteMany: async (args) => {
247
- const coll = params.getCollection(params.table as string);
248
- const filter = normalizeIdFilter(toMongoFilter(args?.where ?? {}));
249
- const result = await coll.deleteMany(filter as any);
250
- return { count: result.deletedCount ?? 0 };
251
- },
252
- count: async (args) => {
253
- const coll = params.getCollection(params.table as string);
254
- const filter = normalizeIdFilter(toMongoFilter(args?.where ?? {}));
255
- return coll.countDocuments(filter ?? {});
256
- },
257
- aggregate: async (args) => {
258
- const coll = params.getCollection(params.table as string);
259
- const pipeline: Document[] = [];
260
-
261
- const normalizedWhere = args.where
262
- ? normalizeIdFilter(
263
- toMongoFilter(
264
- normalizeRefFields(
265
- params.table as string,
266
- args.where as Record<string, unknown>,
267
- params.refs
268
- ) as any
269
- )
270
- )
271
- : undefined;
272
- if (normalizedWhere && Object.keys(normalizedWhere).length > 0) {
273
- pipeline.push({ $match: normalizedWhere });
274
- }
275
-
276
- const sumFields = toArray(args.sum).filter(Boolean) as string[];
277
- const avgFields = toArray(args.avg).filter(Boolean) as string[];
278
- if (sumFields.length === 0 && avgFields.length === 0) {
279
- throw new Error("aggregate requires at least one sum or avg field");
280
- }
281
-
282
- const groupKeys = toArray(args.groupBy).filter(Boolean) as string[];
283
- const groupId =
284
- groupKeys.length === 0
285
- ? null
286
- : groupKeys.length === 1
287
- ? `$${groupKeys[0]}`
288
- : Object.fromEntries(groupKeys.map((k) => [k, `$${k}`]));
289
-
290
- const groupStage: Record<string, unknown> = { _id: groupId };
291
- for (const field of sumFields) {
292
- groupStage[`sum_${field}`] = { $sum: `$${field}` };
293
- }
294
- for (const field of avgFields) {
295
- groupStage[`avg_${field}`] = { $avg: `$${field}` };
296
- }
297
- pipeline.push({ $group: groupStage });
298
-
299
- const tableRefs = params.refs.get(params.table as string) ?? new Map();
300
- const populateKeys = toKeyList(args.populate) ?? [];
301
- for (const key of populateKeys) {
302
- if (!groupKeys.includes(key)) continue;
303
- const target = tableRefs.get(key);
304
- if (!target) continue;
305
- const useRootId = groupKeys.length === 1 && groupKeys[0] === key;
306
- const localField = useRootId ? "_id" : `_id.${key}`;
307
- pipeline.push({
308
- $lookup: {
309
- from: target,
310
- localField,
311
- foreignField: "_id",
312
- as: key,
313
- },
314
- });
315
- pipeline.push({
316
- $unwind: {
317
- path: `$${key}`,
318
- preserveNullAndEmptyArrays: true,
319
- },
320
- });
321
- }
322
-
323
- const results = await coll.aggregate(pipeline as any).toArray();
324
- return results.map((doc) => stringifyAggregateValue(doc)) as any;
325
- },
326
- };
327
- }
@@ -1,234 +0,0 @@
1
- import type { Collection, Document } from "mongodb";
2
- import { ObjectId } from "mongodb";
3
- import { normalizeIdValue, stringifyIdField } from "../utils/id-utils";
4
- import type { SchemaRefMap } from "../types/types";
5
-
6
- export async function applyPopulate(params: {
7
- docs: Array<Record<string, unknown>>;
8
- currentTable: string;
9
- populateKeys: string[];
10
- aggregate?: Record<
11
- string,
12
- { count?: boolean; sum?: string[]; avg?: string[] }
13
- >;
14
- includeDocs?: Record<string, boolean>;
15
- selectedKeys: string[] | undefined;
16
- refs: SchemaRefMap;
17
- getCollection: (table: string) => Collection<Document>;
18
- }) {
19
- const tableRefs = params.refs.get(params.currentTable) ?? new Map();
20
- const aggregateConfig = params.aggregate ?? {};
21
- const includeDocsConfig = params.includeDocs ?? {};
22
-
23
- const ensureStringId = (val: unknown): string | null => {
24
- if (!val) return null;
25
- if (typeof val === "string") return val;
26
- if (val instanceof ObjectId) return val.toHexString();
27
- if (typeof val === "object" && "_id" in (val as any)) {
28
- const maybeId = (val as any)._id;
29
- if (typeof maybeId === "string") return maybeId;
30
- if (maybeId instanceof ObjectId) return maybeId.toHexString();
31
- }
32
- return null;
33
- };
34
-
35
- const docIds = params.docs
36
- .map((doc) => {
37
- const id = ensureStringId(doc._id);
38
- return id ? normalizeIdValue(id) : null;
39
- })
40
- .filter((v): v is string | ObjectId => Boolean(v));
41
-
42
- if (docIds.length === 0) return;
43
-
44
- // Build reverse ref lookup so populate can work even when the current table
45
- // doesn't store the forward reference (e.g., populate tickets on users by
46
- // matching tickets.user -> users._id).
47
- const reverseRefs = new Map<string, { table: string; field: string }[]>();
48
- for (const [table, refs] of params.refs.entries()) {
49
- for (const [field, refTable] of refs.entries()) {
50
- const list = reverseRefs.get(refTable) ?? [];
51
- list.push({ table, field });
52
- reverseRefs.set(refTable, list);
53
- }
54
- }
55
-
56
- const buildLookupMap = (rows: Array<Record<string, unknown>>) => {
57
- const byId = new Map<string, Record<string, unknown>[]>();
58
- for (const row of rows) {
59
- const id = ensureStringId(row._id);
60
- if (!id) continue;
61
- const items = Array.isArray((row as any).__pop)
62
- ? ((row as any).__pop as Array<Record<string, unknown>>)
63
- : [];
64
- items.forEach(stringifyIdField);
65
- byId.set(id, items);
66
- }
67
- return byId;
68
- };
69
-
70
- const computeAggregates = (
71
- value: unknown,
72
- spec: { count?: boolean; sum?: string[]; avg?: string[] }
73
- ): Record<string, number> | null => {
74
- const items = Array.isArray(value)
75
- ? value
76
- : value === undefined || value === null
77
- ? []
78
- : [value];
79
-
80
- const out: Record<string, number> = {};
81
- const numericSum = (field: string) => {
82
- let total = 0;
83
- let found = 0;
84
- for (const item of items) {
85
- if (!item || typeof item !== "object") continue;
86
- const raw = (item as Record<string, unknown>)[field];
87
- if (typeof raw === "number" || typeof raw === "bigint") {
88
- total += Number(raw);
89
- found += 1;
90
- }
91
- }
92
- return { total, found };
93
- };
94
-
95
- if (spec.count) out.count = items.length;
96
-
97
- for (const field of spec.sum ?? []) {
98
- const { total, found } = numericSum(String(field));
99
- if (found > 0) out[`sum_${String(field)}`] = total;
100
- }
101
-
102
- for (const field of spec.avg ?? []) {
103
- const { total, found } = numericSum(String(field));
104
- if (found > 0) out[`avg_${String(field)}`] = total / found;
105
- }
106
-
107
- return Object.keys(out).length ? out : null;
108
- };
109
-
110
- for (const key of params.populateKeys) {
111
- const forwardTarget = tableRefs.get(key);
112
- const backwardCandidates = reverseRefs.get(params.currentTable) ?? [];
113
- const backward = backwardCandidates.find((c) => c.table === key);
114
- const includeDocs = includeDocsConfig[key] ?? true;
115
- let valueById: Map<string, Record<string, unknown>[]> | undefined;
116
- let handled = false;
117
-
118
- // Prefer forward populate via $lookup when the table carries the ref.
119
- if (forwardTarget) {
120
- const hasForwardValues = params.docs.some((doc) => {
121
- const value = doc[key];
122
- return Array.isArray(value)
123
- ? value.length > 0
124
- : value !== undefined && value !== null;
125
- });
126
-
127
- if (hasForwardValues) {
128
- const coll = params.getCollection(params.currentTable);
129
- const pipeline = [
130
- { $match: { _id: { $in: docIds } } },
131
- {
132
- $lookup: {
133
- from: forwardTarget,
134
- localField: key,
135
- foreignField: "_id",
136
- as: "__pop",
137
- },
138
- },
139
- { $project: { _id: 1, __pop: 1 } },
140
- ];
141
-
142
- const populated = (await coll
143
- .aggregate(pipeline as any)
144
- .toArray()) as Array<Record<string, unknown>>;
145
-
146
- const byId = buildLookupMap(populated);
147
- valueById = byId;
148
- handled = true;
149
-
150
- if (includeDocs) {
151
- for (const doc of params.docs) {
152
- const docId = ensureStringId(doc._id);
153
- if (!docId) continue;
154
- const populatedDocs = byId.get(docId) ?? [];
155
- const currentValue = doc[key];
156
-
157
- if (Array.isArray(currentValue)) {
158
- const byRelId = new Map<string, Record<string, unknown>>();
159
- for (const rel of populatedDocs) {
160
- const relId = ensureStringId(rel._id);
161
- if (relId) byRelId.set(relId, rel);
162
- }
163
- doc[key] = currentValue
164
- .map((v) => {
165
- const relId = ensureStringId(v);
166
- return relId ? (byRelId.get(relId) ?? null) : null;
167
- })
168
- .filter((v): v is Record<string, unknown> => Boolean(v));
169
- continue;
170
- }
171
-
172
- const relId = ensureStringId(currentValue);
173
- doc[key] = relId
174
- ? (populatedDocs.find((v) => ensureStringId(v._id) === relId) ??
175
- currentValue)
176
- : (populatedDocs[0] ?? currentValue);
177
- }
178
- }
179
-
180
- continue;
181
- }
182
-
183
- if (!backward) continue;
184
- }
185
-
186
- // Backward populate: find docs in another table that reference this table's _id.
187
- if (backward) {
188
- const coll = params.getCollection(params.currentTable);
189
- const pipeline = [
190
- { $match: { _id: { $in: docIds } } },
191
- {
192
- $lookup: {
193
- from: backward.table,
194
- localField: "_id",
195
- foreignField: backward.field,
196
- as: "__pop",
197
- },
198
- },
199
- { $project: { _id: 1, __pop: 1 } },
200
- ];
201
- const populated = (await coll
202
- .aggregate(pipeline as any)
203
- .toArray()) as Array<Record<string, unknown>>;
204
- if (!handled && backward) {
205
- const grouped = buildLookupMap(populated);
206
- valueById = grouped;
207
-
208
- if (includeDocs) {
209
- for (const doc of params.docs) {
210
- const id = ensureStringId(doc._id);
211
- if (!id) continue;
212
- doc[key] = grouped.get(id) ?? [];
213
- }
214
- }
215
- }
216
- }
217
-
218
- const aggregateSpec = aggregateConfig[key];
219
- if (aggregateSpec) {
220
- for (const doc of params.docs) {
221
- const docId = ensureStringId(doc._id);
222
- if (!docId) continue;
223
- const aggregateSource = valueById?.get(docId) ?? doc[key];
224
- const aggregated = computeAggregates(aggregateSource, aggregateSpec);
225
- if (!aggregated) continue;
226
- const existing = (doc as any)._aggregates as
227
- | Record<string, unknown>
228
- | undefined;
229
- const target = existing ?? ((doc as any)._aggregates = {});
230
- target[key] = aggregated;
231
- }
232
- }
233
- }
234
- }