appflare 0.0.28 → 0.1.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 (151) 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 +747 -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 +353 -0
  34. package/cli/templates/handlers/auth.ts +37 -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/generators/registration/modules/cron.ts +30 -0
  46. package/cli/templates/handlers/generators/registration/modules/realtime.ts +631 -0
  47. package/cli/templates/handlers/generators/registration/modules/scheduler.ts +56 -0
  48. package/cli/templates/handlers/generators/registration/modules/storage.ts +194 -0
  49. package/cli/templates/handlers/generators/registration/sections.ts +210 -0
  50. package/cli/templates/handlers/generators/types/context.ts +64 -0
  51. package/cli/templates/handlers/generators/types/core.ts +101 -0
  52. package/cli/templates/handlers/generators/types/operations.ts +135 -0
  53. package/cli/templates/handlers/generators/types/query-definitions.ts +1029 -0
  54. package/cli/templates/handlers/generators/types/query-runtime.ts +417 -0
  55. package/cli/templates/handlers/index.ts +43 -0
  56. package/cli/templates/handlers/operations.ts +116 -0
  57. package/cli/templates/handlers/registration.ts +83 -0
  58. package/cli/templates/handlers/types.ts +15 -0
  59. package/cli/templates/handlers/utils.ts +48 -0
  60. package/cli/types.ts +108 -0
  61. package/cli/utils/handler-discovery.ts +366 -0
  62. package/cli/utils/json-utils.ts +24 -0
  63. package/cli/utils/path-utils.ts +19 -0
  64. package/cli/utils/schema-discovery.ts +390 -0
  65. package/index.ts +27 -4
  66. package/package.json +23 -20
  67. package/react/index.ts +5 -3
  68. package/react/use-infinite-query.ts +190 -0
  69. package/react/use-mutation.ts +54 -0
  70. package/react/use-query.ts +158 -0
  71. package/schema.ts +262 -0
  72. package/tsconfig.json +2 -4
  73. package/cli/README.md +0 -108
  74. package/cli/core/build.ts +0 -187
  75. package/cli/core/config.ts +0 -92
  76. package/cli/core/discover-handlers.ts +0 -143
  77. package/cli/core/handlers.ts +0 -7
  78. package/cli/core/index.ts +0 -205
  79. package/cli/generators/generate-api-client/client.ts +0 -163
  80. package/cli/generators/generate-api-client/extract-configuration.ts +0 -121
  81. package/cli/generators/generate-api-client/index.ts +0 -973
  82. package/cli/generators/generate-api-client/types.ts +0 -164
  83. package/cli/generators/generate-api-client/utils.ts +0 -22
  84. package/cli/generators/generate-api-client.ts +0 -1
  85. package/cli/generators/generate-cloudflare-worker/helpers.ts +0 -24
  86. package/cli/generators/generate-cloudflare-worker/index.ts +0 -2
  87. package/cli/generators/generate-cloudflare-worker/worker.ts +0 -148
  88. package/cli/generators/generate-cloudflare-worker/wrangler.ts +0 -108
  89. package/cli/generators/generate-cloudflare-worker.ts +0 -4
  90. package/cli/generators/generate-cron-handlers/cron-handlers-block.ts +0 -2
  91. package/cli/generators/generate-cron-handlers/handler-entries.ts +0 -29
  92. package/cli/generators/generate-cron-handlers/index.ts +0 -61
  93. package/cli/generators/generate-cron-handlers/runtime-block.ts +0 -49
  94. package/cli/generators/generate-cron-handlers/type-helpers-block.ts +0 -60
  95. package/cli/generators/generate-db-handlers/index.ts +0 -33
  96. package/cli/generators/generate-db-handlers/prepare.ts +0 -24
  97. package/cli/generators/generate-db-handlers/templates.ts +0 -189
  98. package/cli/generators/generate-db-handlers.ts +0 -1
  99. package/cli/generators/generate-hono-server/auth.ts +0 -97
  100. package/cli/generators/generate-hono-server/imports.ts +0 -55
  101. package/cli/generators/generate-hono-server/index.ts +0 -52
  102. package/cli/generators/generate-hono-server/routes.ts +0 -115
  103. package/cli/generators/generate-hono-server/template.ts +0 -371
  104. package/cli/generators/generate-hono-server.ts +0 -1
  105. package/cli/generators/generate-scheduler-handlers/constants.ts +0 -8
  106. package/cli/generators/generate-scheduler-handlers/handler-entries.ts +0 -22
  107. package/cli/generators/generate-scheduler-handlers/index.ts +0 -51
  108. package/cli/generators/generate-scheduler-handlers/runtime-block.ts +0 -68
  109. package/cli/generators/generate-scheduler-handlers/scheduler-handlers-block.ts +0 -2
  110. package/cli/generators/generate-scheduler-handlers/type-helpers-block.ts +0 -68
  111. package/cli/generators/generate-scheduler-handlers.ts +0 -1
  112. package/cli/generators/generate-websocket-durable-object/auth.ts +0 -30
  113. package/cli/generators/generate-websocket-durable-object/imports.ts +0 -55
  114. package/cli/generators/generate-websocket-durable-object/index.ts +0 -41
  115. package/cli/generators/generate-websocket-durable-object/query-handlers.ts +0 -18
  116. package/cli/generators/generate-websocket-durable-object/template.ts +0 -714
  117. package/cli/generators/generate-websocket-durable-object.ts +0 -1
  118. package/cli/schema/schema-static-types.ts +0 -702
  119. package/cli/schema/schema.ts +0 -151
  120. package/cli/utils/tsc.ts +0 -54
  121. package/cli/utils/utils.ts +0 -190
  122. package/cli/utils/zod-utils.ts +0 -121
  123. package/lib/README.md +0 -50
  124. package/lib/db.ts +0 -19
  125. package/lib/location.ts +0 -110
  126. package/lib/values.ts +0 -27
  127. package/react/README.md +0 -67
  128. package/react/hooks/useMutation.ts +0 -89
  129. package/react/hooks/usePaginatedQuery.ts +0 -213
  130. package/react/hooks/useQuery.ts +0 -106
  131. package/react/shared/queryShared.ts +0 -174
  132. package/server/README.md +0 -218
  133. package/server/auth.ts +0 -107
  134. package/server/database/builders.ts +0 -83
  135. package/server/database/context.ts +0 -327
  136. package/server/database/populate.ts +0 -234
  137. package/server/database/query-builder.ts +0 -161
  138. package/server/database/query-utils.ts +0 -25
  139. package/server/db.ts +0 -2
  140. package/server/storage/auth.ts +0 -16
  141. package/server/storage/bucket.ts +0 -22
  142. package/server/storage/context.ts +0 -34
  143. package/server/storage/index.ts +0 -38
  144. package/server/storage/operations.ts +0 -149
  145. package/server/storage/route-handler.ts +0 -60
  146. package/server/storage/types.ts +0 -55
  147. package/server/storage/utils.ts +0 -47
  148. package/server/storage.ts +0 -6
  149. package/server/types/schema-refs.ts +0 -66
  150. package/server/types/types.ts +0 -633
  151. 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
- }