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.
- package/cli/commands/index.ts +140 -0
- package/cli/generate.ts +149 -0
- package/cli/index.ts +56 -447
- package/cli/load-config.ts +182 -0
- package/cli/schema-compiler.ts +657 -0
- package/cli/templates/auth/README.md +156 -0
- package/cli/templates/auth/config.ts +61 -0
- package/cli/templates/auth/route-config.ts +18 -0
- package/cli/templates/auth/route-handler.ts +18 -0
- package/cli/templates/auth/route-request-utils.ts +55 -0
- package/cli/templates/auth/route.ts +14 -0
- package/cli/templates/core/README.md +266 -0
- package/cli/templates/core/app-creation.ts +19 -0
- package/cli/templates/core/client/appflare.ts +37 -0
- package/cli/templates/core/client/index.ts +6 -0
- package/cli/templates/core/client/storage.ts +100 -0
- package/cli/templates/core/client/types.ts +54 -0
- package/cli/templates/core/client-modules/appflare.ts +112 -0
- package/cli/templates/core/client-modules/handlers/index.ts +740 -0
- package/cli/templates/core/client-modules/handlers.ts +1 -0
- package/cli/templates/core/client-modules/index.ts +7 -0
- package/cli/templates/core/client-modules/storage.ts +180 -0
- package/cli/templates/core/client-modules/types.ts +145 -0
- package/cli/templates/core/client.ts +39 -0
- package/cli/templates/core/drizzle.ts +15 -0
- package/cli/templates/core/export.ts +14 -0
- package/cli/templates/core/handlers-route.ts +23 -0
- package/cli/templates/core/handlers.ts +1 -0
- package/cli/templates/core/imports.ts +8 -0
- package/cli/templates/core/server.ts +38 -0
- package/cli/templates/core/types.ts +6 -0
- package/cli/templates/core/wrangler.ts +109 -0
- package/cli/templates/handlers/README.md +265 -0
- package/cli/templates/handlers/auth.ts +36 -0
- package/cli/templates/handlers/execution.ts +39 -0
- package/cli/templates/handlers/generators/context/context-creation.ts +80 -0
- package/cli/templates/handlers/generators/context/error-helpers.ts +11 -0
- package/cli/templates/handlers/generators/context/scheduler.ts +24 -0
- package/cli/templates/handlers/generators/context/storage-api.ts +112 -0
- package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -0
- package/cli/templates/handlers/generators/context/types.ts +18 -0
- package/cli/templates/handlers/generators/context.ts +43 -0
- package/cli/templates/handlers/generators/execution.ts +15 -0
- package/cli/templates/handlers/generators/handlers.ts +13 -0
- package/cli/templates/handlers/index.ts +43 -0
- package/cli/templates/handlers/operations.ts +116 -0
- package/cli/templates/handlers/registration.ts +1114 -0
- package/cli/templates/handlers/types.ts +960 -0
- package/cli/templates/handlers/utils.ts +48 -0
- package/cli/types.ts +108 -0
- package/cli/utils/handler-discovery.ts +366 -0
- package/cli/utils/json-utils.ts +24 -0
- package/cli/utils/path-utils.ts +19 -0
- package/cli/utils/schema-discovery.ts +390 -0
- package/index.ts +27 -4
- package/package.json +23 -20
- package/react/index.ts +5 -3
- package/react/use-infinite-query.ts +190 -0
- package/react/use-mutation.ts +54 -0
- package/react/use-query.ts +158 -0
- package/schema.ts +262 -0
- package/tsconfig.json +2 -4
- package/cli/README.md +0 -108
- package/cli/core/build.ts +0 -187
- package/cli/core/config.ts +0 -92
- package/cli/core/discover-handlers.ts +0 -143
- package/cli/core/handlers.ts +0 -7
- package/cli/core/index.ts +0 -205
- package/cli/generators/generate-api-client/client.ts +0 -163
- package/cli/generators/generate-api-client/extract-configuration.ts +0 -121
- package/cli/generators/generate-api-client/index.ts +0 -973
- package/cli/generators/generate-api-client/types.ts +0 -164
- package/cli/generators/generate-api-client/utils.ts +0 -22
- package/cli/generators/generate-api-client.ts +0 -1
- package/cli/generators/generate-cloudflare-worker/helpers.ts +0 -24
- package/cli/generators/generate-cloudflare-worker/index.ts +0 -2
- package/cli/generators/generate-cloudflare-worker/worker.ts +0 -148
- package/cli/generators/generate-cloudflare-worker/wrangler.ts +0 -108
- package/cli/generators/generate-cloudflare-worker.ts +0 -4
- package/cli/generators/generate-cron-handlers/cron-handlers-block.ts +0 -2
- package/cli/generators/generate-cron-handlers/handler-entries.ts +0 -29
- package/cli/generators/generate-cron-handlers/index.ts +0 -61
- package/cli/generators/generate-cron-handlers/runtime-block.ts +0 -49
- package/cli/generators/generate-cron-handlers/type-helpers-block.ts +0 -60
- package/cli/generators/generate-db-handlers/index.ts +0 -33
- package/cli/generators/generate-db-handlers/prepare.ts +0 -24
- package/cli/generators/generate-db-handlers/templates.ts +0 -189
- package/cli/generators/generate-db-handlers.ts +0 -1
- package/cli/generators/generate-hono-server/auth.ts +0 -97
- package/cli/generators/generate-hono-server/imports.ts +0 -55
- package/cli/generators/generate-hono-server/index.ts +0 -52
- package/cli/generators/generate-hono-server/routes.ts +0 -115
- package/cli/generators/generate-hono-server/template.ts +0 -371
- package/cli/generators/generate-hono-server.ts +0 -1
- package/cli/generators/generate-scheduler-handlers/constants.ts +0 -8
- package/cli/generators/generate-scheduler-handlers/handler-entries.ts +0 -22
- package/cli/generators/generate-scheduler-handlers/index.ts +0 -51
- package/cli/generators/generate-scheduler-handlers/runtime-block.ts +0 -68
- package/cli/generators/generate-scheduler-handlers/scheduler-handlers-block.ts +0 -2
- package/cli/generators/generate-scheduler-handlers/type-helpers-block.ts +0 -68
- package/cli/generators/generate-scheduler-handlers.ts +0 -1
- package/cli/generators/generate-websocket-durable-object/auth.ts +0 -30
- package/cli/generators/generate-websocket-durable-object/imports.ts +0 -55
- package/cli/generators/generate-websocket-durable-object/index.ts +0 -41
- package/cli/generators/generate-websocket-durable-object/query-handlers.ts +0 -18
- package/cli/generators/generate-websocket-durable-object/template.ts +0 -714
- package/cli/generators/generate-websocket-durable-object.ts +0 -1
- package/cli/schema/schema-static-types.ts +0 -702
- package/cli/schema/schema.ts +0 -151
- package/cli/utils/tsc.ts +0 -54
- package/cli/utils/utils.ts +0 -190
- package/cli/utils/zod-utils.ts +0 -121
- package/lib/README.md +0 -50
- package/lib/db.ts +0 -19
- package/lib/location.ts +0 -110
- package/lib/values.ts +0 -27
- package/react/README.md +0 -67
- package/react/hooks/useMutation.ts +0 -89
- package/react/hooks/usePaginatedQuery.ts +0 -213
- package/react/hooks/useQuery.ts +0 -106
- package/react/shared/queryShared.ts +0 -174
- package/server/README.md +0 -218
- package/server/auth.ts +0 -107
- package/server/database/builders.ts +0 -83
- package/server/database/context.ts +0 -327
- package/server/database/populate.ts +0 -234
- package/server/database/query-builder.ts +0 -161
- package/server/database/query-utils.ts +0 -25
- package/server/db.ts +0 -2
- package/server/storage/auth.ts +0 -16
- package/server/storage/bucket.ts +0 -22
- package/server/storage/context.ts +0 -34
- package/server/storage/index.ts +0 -38
- package/server/storage/operations.ts +0 -149
- package/server/storage/route-handler.ts +0 -60
- package/server/storage/types.ts +0 -55
- package/server/storage/utils.ts +0 -47
- package/server/storage.ts +0 -6
- package/server/types/schema-refs.ts +0 -66
- package/server/types/types.ts +0 -633
- 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
|
-
}
|