appflare 0.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.
- package/cli/README.md +101 -0
- package/cli/core/build.ts +136 -0
- package/cli/core/config.ts +29 -0
- package/cli/core/discover-handlers.ts +61 -0
- package/cli/core/handlers.ts +5 -0
- package/cli/core/index.ts +157 -0
- package/cli/generators/generate-api-client/client.ts +93 -0
- package/cli/generators/generate-api-client/index.ts +529 -0
- package/cli/generators/generate-api-client/types.ts +59 -0
- package/cli/generators/generate-api-client/utils.ts +18 -0
- package/cli/generators/generate-api-client.ts +1 -0
- package/cli/generators/generate-db-handlers.ts +138 -0
- package/cli/generators/generate-hono-server.ts +238 -0
- package/cli/generators/generate-websocket-durable-object.ts +537 -0
- package/cli/index.ts +157 -0
- package/cli/schema/schema-static-types.ts +252 -0
- package/cli/schema/schema.ts +105 -0
- package/cli/utils/tsc.ts +53 -0
- package/cli/utils/utils.ts +126 -0
- package/cli/utils/zod-utils.ts +115 -0
- package/index.ts +2 -0
- package/lib/README.md +43 -0
- package/lib/db.ts +9 -0
- package/lib/values.ts +23 -0
- package/package.json +28 -0
- package/react/README.md +67 -0
- package/react/hooks/useMutation.ts +89 -0
- package/react/hooks/usePaginatedQuery.ts +213 -0
- package/react/hooks/useQuery.ts +106 -0
- package/react/index.ts +3 -0
- package/react/shared/queryShared.ts +169 -0
- package/server/README.md +153 -0
- package/server/database/builders.ts +83 -0
- package/server/database/context.ts +265 -0
- package/server/database/populate.ts +160 -0
- package/server/database/query-builder.ts +101 -0
- package/server/database/query-utils.ts +25 -0
- package/server/db.ts +2 -0
- package/server/types/schema-refs.ts +66 -0
- package/server/types/types.ts +419 -0
- package/server/utils/id-utils.ts +123 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
selectedKeys: string[] | undefined;
|
|
11
|
+
refs: SchemaRefMap;
|
|
12
|
+
getCollection: (table: string) => Collection<Document>;
|
|
13
|
+
}) {
|
|
14
|
+
const tableRefs = params.refs.get(params.currentTable) ?? new Map();
|
|
15
|
+
|
|
16
|
+
const ensureStringId = (val: unknown): string | null => {
|
|
17
|
+
if (!val) return null;
|
|
18
|
+
if (typeof val === "string") return val;
|
|
19
|
+
if (val instanceof ObjectId) return val.toHexString();
|
|
20
|
+
if (typeof val === "object" && "_id" in (val as any)) {
|
|
21
|
+
const maybeId = (val as any)._id;
|
|
22
|
+
if (typeof maybeId === "string") return maybeId;
|
|
23
|
+
if (maybeId instanceof ObjectId) return maybeId.toHexString();
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const docIds = params.docs
|
|
29
|
+
.map((doc) => {
|
|
30
|
+
const id = ensureStringId(doc._id);
|
|
31
|
+
return id ? normalizeIdValue(id) : null;
|
|
32
|
+
})
|
|
33
|
+
.filter((v): v is string | ObjectId => Boolean(v));
|
|
34
|
+
|
|
35
|
+
if (docIds.length === 0) return;
|
|
36
|
+
|
|
37
|
+
// Build reverse ref lookup so populate can work even when the current table
|
|
38
|
+
// doesn't store the forward reference (e.g., populate tickets on users by
|
|
39
|
+
// matching tickets.user -> users._id).
|
|
40
|
+
const reverseRefs = new Map<string, { table: string; field: string }[]>();
|
|
41
|
+
for (const [table, refs] of params.refs.entries()) {
|
|
42
|
+
for (const [field, refTable] of refs.entries()) {
|
|
43
|
+
const list = reverseRefs.get(refTable) ?? [];
|
|
44
|
+
list.push({ table, field });
|
|
45
|
+
reverseRefs.set(refTable, list);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const buildLookupMap = (rows: Array<Record<string, unknown>>) => {
|
|
50
|
+
const byId = new Map<string, Record<string, unknown>[]>();
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
const id = ensureStringId(row._id);
|
|
53
|
+
if (!id) continue;
|
|
54
|
+
const items = Array.isArray((row as any).__pop)
|
|
55
|
+
? ((row as any).__pop as Array<Record<string, unknown>>)
|
|
56
|
+
: [];
|
|
57
|
+
items.forEach(stringifyIdField);
|
|
58
|
+
byId.set(id, items);
|
|
59
|
+
}
|
|
60
|
+
return byId;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
for (const key of params.populateKeys) {
|
|
64
|
+
const forwardTarget = tableRefs.get(key);
|
|
65
|
+
const backwardCandidates = reverseRefs.get(params.currentTable) ?? [];
|
|
66
|
+
const backward = backwardCandidates.find((c) => c.table === key);
|
|
67
|
+
|
|
68
|
+
// Prefer forward populate via $lookup when the table carries the ref.
|
|
69
|
+
if (forwardTarget) {
|
|
70
|
+
const hasForwardValues = params.docs.some((doc) => {
|
|
71
|
+
const value = doc[key];
|
|
72
|
+
return Array.isArray(value)
|
|
73
|
+
? value.length > 0
|
|
74
|
+
: value !== undefined && value !== null;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (hasForwardValues) {
|
|
78
|
+
const coll = params.getCollection(params.currentTable);
|
|
79
|
+
const pipeline = [
|
|
80
|
+
{ $match: { _id: { $in: docIds } } },
|
|
81
|
+
{
|
|
82
|
+
$lookup: {
|
|
83
|
+
from: forwardTarget,
|
|
84
|
+
localField: key,
|
|
85
|
+
foreignField: "_id",
|
|
86
|
+
as: "__pop",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{ $project: { _id: 1, __pop: 1 } },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const populated = (await coll
|
|
93
|
+
.aggregate(pipeline as any)
|
|
94
|
+
.toArray()) as Array<Record<string, unknown>>;
|
|
95
|
+
|
|
96
|
+
const byId = buildLookupMap(populated);
|
|
97
|
+
|
|
98
|
+
for (const doc of params.docs) {
|
|
99
|
+
const docId = ensureStringId(doc._id);
|
|
100
|
+
if (!docId) continue;
|
|
101
|
+
const populatedDocs = byId.get(docId) ?? [];
|
|
102
|
+
const currentValue = doc[key];
|
|
103
|
+
|
|
104
|
+
if (Array.isArray(currentValue)) {
|
|
105
|
+
const byRelId = new Map<string, Record<string, unknown>>();
|
|
106
|
+
for (const rel of populatedDocs) {
|
|
107
|
+
const relId = ensureStringId(rel._id);
|
|
108
|
+
if (relId) byRelId.set(relId, rel);
|
|
109
|
+
}
|
|
110
|
+
doc[key] = currentValue
|
|
111
|
+
.map((v) => {
|
|
112
|
+
const relId = ensureStringId(v);
|
|
113
|
+
return relId ? (byRelId.get(relId) ?? null) : null;
|
|
114
|
+
})
|
|
115
|
+
.filter((v): v is Record<string, unknown> => Boolean(v));
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const relId = ensureStringId(currentValue);
|
|
120
|
+
doc[key] = relId
|
|
121
|
+
? (populatedDocs.find((v) => ensureStringId(v._id) === relId) ??
|
|
122
|
+
currentValue)
|
|
123
|
+
: (populatedDocs[0] ?? currentValue);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!backward) continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Backward populate: find docs in another table that reference this table's _id.
|
|
133
|
+
if (backward) {
|
|
134
|
+
const coll = params.getCollection(params.currentTable);
|
|
135
|
+
const pipeline = [
|
|
136
|
+
{ $match: { _id: { $in: docIds } } },
|
|
137
|
+
{
|
|
138
|
+
$lookup: {
|
|
139
|
+
from: backward.table,
|
|
140
|
+
localField: "_id",
|
|
141
|
+
foreignField: backward.field,
|
|
142
|
+
as: "__pop",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{ $project: { _id: 1, __pop: 1 } },
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
const populated = (await coll
|
|
149
|
+
.aggregate(pipeline as any)
|
|
150
|
+
.toArray()) as Array<Record<string, unknown>>;
|
|
151
|
+
|
|
152
|
+
const grouped = buildLookupMap(populated);
|
|
153
|
+
for (const doc of params.docs) {
|
|
154
|
+
const id = ensureStringId(doc._id);
|
|
155
|
+
if (!id) continue;
|
|
156
|
+
doc[key] = grouped.get(id) ?? [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Collection, Document, Filter, FindOptions, Sort } from "mongodb";
|
|
2
|
+
import { applyPopulate } from "./populate";
|
|
3
|
+
import { normalizeIdFilter, stringifyIdField } from "../utils/id-utils";
|
|
4
|
+
import type { MongoDbQuery, SchemaRefMap } from "../types/types";
|
|
5
|
+
import { buildProjection, normalizeSort } from "./query-utils";
|
|
6
|
+
|
|
7
|
+
export function createQueryBuilder<
|
|
8
|
+
TTableNames extends string,
|
|
9
|
+
TTableDocMap extends Record<TTableNames, any>,
|
|
10
|
+
TableName extends TTableNames,
|
|
11
|
+
>(params: {
|
|
12
|
+
table: TableName;
|
|
13
|
+
getCollection: (table: string) => Collection<Document>;
|
|
14
|
+
refs: SchemaRefMap;
|
|
15
|
+
}): MongoDbQuery<TableName, TTableDocMap, TTableDocMap[TableName]> {
|
|
16
|
+
let filter: Filter<Document> | undefined;
|
|
17
|
+
let sort: Sort | undefined;
|
|
18
|
+
let limit: number | undefined;
|
|
19
|
+
let offset: number | undefined;
|
|
20
|
+
let selectedKeys: string[] | undefined;
|
|
21
|
+
let populateKeys: string[] = [];
|
|
22
|
+
|
|
23
|
+
const api: MongoDbQuery<any, any, any> = {
|
|
24
|
+
where(next) {
|
|
25
|
+
const nextFilter = next as unknown as Filter<Document>;
|
|
26
|
+
if (!filter) {
|
|
27
|
+
filter = nextFilter;
|
|
28
|
+
} else {
|
|
29
|
+
filter = { $and: [filter as any, nextFilter as any] } as any;
|
|
30
|
+
}
|
|
31
|
+
return api;
|
|
32
|
+
},
|
|
33
|
+
sort(next) {
|
|
34
|
+
sort = normalizeSort(next as any);
|
|
35
|
+
return api;
|
|
36
|
+
},
|
|
37
|
+
limit(n) {
|
|
38
|
+
limit = n;
|
|
39
|
+
return api;
|
|
40
|
+
},
|
|
41
|
+
offset(n) {
|
|
42
|
+
offset = n;
|
|
43
|
+
return api;
|
|
44
|
+
},
|
|
45
|
+
select(...args) {
|
|
46
|
+
const keys = Array.isArray(args[0])
|
|
47
|
+
? (args[0] as any[])
|
|
48
|
+
: (args as any[]);
|
|
49
|
+
selectedKeys = keys.map(String);
|
|
50
|
+
return api;
|
|
51
|
+
},
|
|
52
|
+
populate(arg: any) {
|
|
53
|
+
const keys = Array.isArray(arg) ? arg : [arg];
|
|
54
|
+
for (const k of keys) {
|
|
55
|
+
const ks = String(k);
|
|
56
|
+
if (!populateKeys.includes(ks)) populateKeys.push(ks);
|
|
57
|
+
}
|
|
58
|
+
return api;
|
|
59
|
+
},
|
|
60
|
+
async find() {
|
|
61
|
+
const coll = params.getCollection(params.table as string);
|
|
62
|
+
const options: FindOptions = {};
|
|
63
|
+
if (selectedKeys) {
|
|
64
|
+
// Ensure referenced ids are available for populate even when the caller selected a subset.
|
|
65
|
+
const projectionKeys = Array.from(
|
|
66
|
+
new Set([...selectedKeys, ...populateKeys])
|
|
67
|
+
);
|
|
68
|
+
options.projection = buildProjection(projectionKeys);
|
|
69
|
+
}
|
|
70
|
+
const normalizedFilter = normalizeIdFilter(filter);
|
|
71
|
+
let cursor = coll.find(normalizedFilter ?? ({} as any), options);
|
|
72
|
+
if (sort) cursor = cursor.sort(sort);
|
|
73
|
+
if (offset !== undefined) cursor = cursor.skip(offset);
|
|
74
|
+
if (limit !== undefined) cursor = cursor.limit(limit);
|
|
75
|
+
const docs = (await cursor.toArray()) as Array<Record<string, unknown>>;
|
|
76
|
+
docs.forEach(stringifyIdField);
|
|
77
|
+
|
|
78
|
+
if (populateKeys.length > 0) {
|
|
79
|
+
await applyPopulate({
|
|
80
|
+
docs,
|
|
81
|
+
currentTable: params.table as string,
|
|
82
|
+
populateKeys,
|
|
83
|
+
selectedKeys,
|
|
84
|
+
refs: params.refs,
|
|
85
|
+
getCollection: params.getCollection,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return docs as any;
|
|
90
|
+
},
|
|
91
|
+
async findOne() {
|
|
92
|
+
if (limit === undefined || limit > 1) {
|
|
93
|
+
limit = 1;
|
|
94
|
+
}
|
|
95
|
+
const result = await api.find();
|
|
96
|
+
return (result[0] ?? null) as any;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return api as any;
|
|
101
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Sort } from "mongodb";
|
|
2
|
+
import type { QuerySort } from "../types/types";
|
|
3
|
+
|
|
4
|
+
export function buildProjection(keys: string[]): Record<string, 0 | 1> {
|
|
5
|
+
const projection: Record<string, 0 | 1> = {};
|
|
6
|
+
for (const k of keys) projection[k] = 1;
|
|
7
|
+
|
|
8
|
+
// Mongo includes _id by default; keep runtime aligned with schema-types `select()`.
|
|
9
|
+
if (!keys.includes("_id")) projection._id = 0;
|
|
10
|
+
if (!keys.includes("_creationTime")) projection._creationTime = 0;
|
|
11
|
+
return projection;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function normalizeSort(sort: QuerySort<string>): Sort {
|
|
15
|
+
if (Array.isArray(sort)) {
|
|
16
|
+
return Object.fromEntries(
|
|
17
|
+
sort.map(([k, dir]) => [k, dir === "desc" ? -1 : 1])
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const out: Record<string, 1 | -1> = {};
|
|
21
|
+
for (const [k, v] of Object.entries(sort ?? {})) {
|
|
22
|
+
out[k] = v === "desc" ? -1 : 1;
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
package/server/db.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { AnyZod, SchemaRefMap } from "./types";
|
|
2
|
+
|
|
3
|
+
export function buildSchemaRefMap(
|
|
4
|
+
schema: Record<string, AnyZod>
|
|
5
|
+
): SchemaRefMap {
|
|
6
|
+
const result: SchemaRefMap = new Map();
|
|
7
|
+
for (const [tableName, validator] of Object.entries(schema)) {
|
|
8
|
+
const tableRefs = new Map<string, string>();
|
|
9
|
+
const shape = getZodObjectShape(validator);
|
|
10
|
+
for (const [field, fieldSchema] of Object.entries(shape)) {
|
|
11
|
+
const ref = extractRefTableName(fieldSchema);
|
|
12
|
+
if (ref) tableRefs.set(field, ref);
|
|
13
|
+
}
|
|
14
|
+
result.set(tableName, tableRefs);
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function extractRefTableName(schema: AnyZod): string | null {
|
|
20
|
+
if (!schema) return null;
|
|
21
|
+
const def = schema?._def;
|
|
22
|
+
const typeName: string | undefined = def?.typeName ?? def?.type;
|
|
23
|
+
|
|
24
|
+
if (typeName === "ZodOptional" || typeName === "optional") {
|
|
25
|
+
return extractRefTableName(
|
|
26
|
+
def?.innerType ?? def?.schema ?? schema?._def?.innerType
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (typeName === "ZodNullable" || typeName === "nullable") {
|
|
30
|
+
return extractRefTableName(def?.innerType ?? def?.schema);
|
|
31
|
+
}
|
|
32
|
+
if (typeName === "ZodDefault" || typeName === "default") {
|
|
33
|
+
return extractRefTableName(def?.innerType ?? def?.schema);
|
|
34
|
+
}
|
|
35
|
+
if (typeName === "ZodArray" || typeName === "array") {
|
|
36
|
+
return extractRefTableName(def?.element ?? def?.innerType ?? def?.type);
|
|
37
|
+
}
|
|
38
|
+
if (typeName === "ZodString" || typeName === "string") {
|
|
39
|
+
const description: string | undefined =
|
|
40
|
+
schema?.description ?? def?.description;
|
|
41
|
+
if (typeof description === "string" && description.startsWith("ref:")) {
|
|
42
|
+
return description.slice("ref:".length);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getZodObjectShape(schema: AnyZod): Record<string, AnyZod> {
|
|
51
|
+
if (!schema || typeof schema !== "object") {
|
|
52
|
+
throw new Error(`Schema table is not an object`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const def = schema?._def;
|
|
56
|
+
if (def?.typeName === "ZodObject" || def?.type === "object") {
|
|
57
|
+
const shape = def.shape;
|
|
58
|
+
if (typeof shape === "function") return shape();
|
|
59
|
+
if (shape && typeof shape === "object") return shape;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof schema.shape === "function") return schema.shape();
|
|
63
|
+
if (schema.shape && typeof schema.shape === "object") return schema.shape;
|
|
64
|
+
|
|
65
|
+
throw new Error(`Table schema is not a Zod object`);
|
|
66
|
+
}
|