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.
Files changed (42) hide show
  1. package/cli/README.md +101 -0
  2. package/cli/core/build.ts +136 -0
  3. package/cli/core/config.ts +29 -0
  4. package/cli/core/discover-handlers.ts +61 -0
  5. package/cli/core/handlers.ts +5 -0
  6. package/cli/core/index.ts +157 -0
  7. package/cli/generators/generate-api-client/client.ts +93 -0
  8. package/cli/generators/generate-api-client/index.ts +529 -0
  9. package/cli/generators/generate-api-client/types.ts +59 -0
  10. package/cli/generators/generate-api-client/utils.ts +18 -0
  11. package/cli/generators/generate-api-client.ts +1 -0
  12. package/cli/generators/generate-db-handlers.ts +138 -0
  13. package/cli/generators/generate-hono-server.ts +238 -0
  14. package/cli/generators/generate-websocket-durable-object.ts +537 -0
  15. package/cli/index.ts +157 -0
  16. package/cli/schema/schema-static-types.ts +252 -0
  17. package/cli/schema/schema.ts +105 -0
  18. package/cli/utils/tsc.ts +53 -0
  19. package/cli/utils/utils.ts +126 -0
  20. package/cli/utils/zod-utils.ts +115 -0
  21. package/index.ts +2 -0
  22. package/lib/README.md +43 -0
  23. package/lib/db.ts +9 -0
  24. package/lib/values.ts +23 -0
  25. package/package.json +28 -0
  26. package/react/README.md +67 -0
  27. package/react/hooks/useMutation.ts +89 -0
  28. package/react/hooks/usePaginatedQuery.ts +213 -0
  29. package/react/hooks/useQuery.ts +106 -0
  30. package/react/index.ts +3 -0
  31. package/react/shared/queryShared.ts +169 -0
  32. package/server/README.md +153 -0
  33. package/server/database/builders.ts +83 -0
  34. package/server/database/context.ts +265 -0
  35. package/server/database/populate.ts +160 -0
  36. package/server/database/query-builder.ts +101 -0
  37. package/server/database/query-utils.ts +25 -0
  38. package/server/db.ts +2 -0
  39. package/server/types/schema-refs.ts +66 -0
  40. package/server/types/types.ts +419 -0
  41. package/server/utils/id-utils.ts +123 -0
  42. package/tsconfig.json +7 -0
@@ -0,0 +1,169 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { QueryKey } from "@tanstack/react-query";
3
+
4
+ export type RealtimeMessage<TResult> = {
5
+ type?: string;
6
+ data?: TResult[];
7
+ [key: string]: unknown;
8
+ };
9
+
10
+ export type HandlerWebsocketOptions<TResult> = {
11
+ baseUrl?: string;
12
+ table?: string;
13
+ handler?: { file: string; name: string };
14
+ handlerFile?: string;
15
+ handlerName?: string;
16
+ where?: Record<string, unknown>;
17
+ orderBy?: Record<string, unknown>;
18
+ take?: number;
19
+ skip?: number;
20
+ path?: string;
21
+ protocols?: string | string[];
22
+ signal?: AbortSignal;
23
+ websocketImpl?: (url: string, protocols?: string | string[]) => WebSocket;
24
+ onOpen?: (event: any) => void;
25
+ onClose?: (event: any) => void;
26
+ onError?: (event: any) => void;
27
+ onMessage?: (message: RealtimeMessage<TResult>, raw: any) => void;
28
+ onData?: (data: TResult[], message: RealtimeMessage<TResult>) => void;
29
+ };
30
+
31
+ export type HandlerWithRealtime<TArgs, TResult> = {
32
+ (args: TArgs, init?: RequestInit): Promise<TResult>;
33
+ websocket?: (
34
+ args?: TArgs,
35
+ options?: HandlerWebsocketOptions<TResult>
36
+ ) => WebSocket;
37
+ schema?: unknown;
38
+ path?: string;
39
+ };
40
+
41
+ export type RealtimeHookOptions<TResult> = HandlerWebsocketOptions<TResult> & {
42
+ enabled?: boolean;
43
+ replaceData?: boolean;
44
+ };
45
+
46
+ type RealtimeOptions<TResult> = {
47
+ enabled: boolean;
48
+ options: RealtimeHookOptions<TResult>;
49
+ };
50
+
51
+ type RealtimeSubscriptionParams<TArgs, TResult> = {
52
+ handler: HandlerWithRealtime<TArgs, TResult>;
53
+ args?: TArgs;
54
+ realtime?: boolean | RealtimeHookOptions<TResult>;
55
+ finalQueryKey: QueryKey;
56
+ deps?: unknown[];
57
+ applyIncoming: (
58
+ data: TResult[] | undefined,
59
+ message: RealtimeMessage<TResult> | undefined
60
+ ) => void;
61
+ };
62
+
63
+ export function stableSerialize(value: unknown): string {
64
+ try {
65
+ return JSON.stringify(value) ?? "";
66
+ } catch {
67
+ return String(value);
68
+ }
69
+ }
70
+
71
+ export function buildQueryKey(
72
+ queryKey: QueryKey | undefined,
73
+ handler: { path?: string },
74
+ argsKey: string
75
+ ): QueryKey {
76
+ return queryKey ?? [handler?.path ?? "appflare-handler", argsKey];
77
+ }
78
+
79
+ export function useRealtimeSubscription<TArgs, TResult>(
80
+ params: RealtimeSubscriptionParams<TArgs, TResult>
81
+ ): WebSocket | null {
82
+ const { handler, args, realtime, finalQueryKey, deps, applyIncoming } =
83
+ params;
84
+ const depsArray = deps;
85
+ const websocketRef = useRef<WebSocket | null>(null);
86
+
87
+ // Track latest realtime options so callbacks stay fresh without forcing reconnects.
88
+ const latestRealtimeOptionsRef = useRef<
89
+ RealtimeHookOptions<TResult> | undefined
90
+ >();
91
+ const realtimeKey = buildRealtimeKey(realtime);
92
+ const parsedRealtime = parseRealtimeOptions<TResult>(realtime);
93
+ latestRealtimeOptionsRef.current = parsedRealtime.options;
94
+
95
+ useEffect(() => {
96
+ const hasWebsocket = typeof handler.websocket === "function";
97
+ const { enabled, options } = parsedRealtime;
98
+
99
+ if (!enabled || !hasWebsocket) {
100
+ return undefined;
101
+ }
102
+
103
+ const socket = handler.websocket!(args, {
104
+ ...options,
105
+ onData: (data, message) => {
106
+ latestRealtimeOptionsRef.current?.onData?.(data, message);
107
+ if (options.replaceData === false) return;
108
+ applyIncoming(data, message);
109
+ },
110
+ onMessage: (message, raw) => {
111
+ latestRealtimeOptionsRef.current?.onMessage?.(message, raw);
112
+ if (
113
+ options.replaceData === false ||
114
+ !message ||
115
+ message.type !== "data" ||
116
+ !Array.isArray((message as any).data)
117
+ ) {
118
+ return;
119
+ }
120
+ applyIncoming((message as any).data, message as any);
121
+ },
122
+ });
123
+
124
+ websocketRef.current = socket;
125
+
126
+ return () => {
127
+ try {
128
+ socket.close(1000, "cleanup");
129
+ } catch {
130
+ // ignore
131
+ }
132
+ };
133
+ }, [handler, finalQueryKey, realtimeKey, applyIncoming, ...depsArray]);
134
+
135
+ return websocketRef.current;
136
+ }
137
+
138
+ function parseRealtimeOptions<TResult>(
139
+ realtime?: boolean | RealtimeHookOptions<TResult>
140
+ ): RealtimeOptions<TResult> {
141
+ const options =
142
+ typeof realtime === "object"
143
+ ? (realtime as RealtimeHookOptions<TResult>)
144
+ : ({} as RealtimeHookOptions<TResult>);
145
+
146
+ const enabled =
147
+ realtime === true ||
148
+ (typeof realtime === "object" && options.enabled !== false);
149
+
150
+ return { enabled, options };
151
+ }
152
+
153
+ function buildRealtimeKey<TResult>(
154
+ realtime?: boolean | RealtimeHookOptions<TResult>
155
+ ): string {
156
+ if (realtime === true) return "true";
157
+ if (!realtime) return "false";
158
+ const {
159
+ onOpen,
160
+ onClose,
161
+ onError,
162
+ onMessage,
163
+ onData,
164
+ websocketImpl,
165
+ signal,
166
+ ...rest
167
+ } = realtime;
168
+ return stableSerialize(rest);
169
+ }
@@ -0,0 +1,153 @@
1
+ # Appflare Server (MongoDB) Module
2
+
3
+ Appflare's server package provides a small MongoDB data layer with typed helpers for querying, writing, and populating referenced documents inferred from your schema. The entrypoint is `createMongoDbContext`, which builds per-table clients that expose a Prisma-like API (`findMany`, `create`, `update`, `delete`, etc.).
4
+
5
+ ## Directory Map
6
+
7
+ - `db.ts`: Public exports for the module (re-exports context and types).
8
+ - `database/context.ts`: Builds the MongoDB context and per-table client facade.
9
+ - `database/builders.ts`: Low-level delete/update/patch builders used by the context.
10
+ - `database/query-builder.ts`: Chainable query API (`where`, `sort`, `limit`, `offset`, `select`, `populate`, `find`, `findOne`).
11
+ - `database/query-utils.ts`: Projection and sort normalization helpers.
12
+ - `database/populate.ts`: Populates referenced documents via forward or reverse lookups.
13
+ - `types/`: Shared TypeScript types for docs, queries, and table clients. `schema-refs.ts` derives reference metadata from Zod schemas.
14
+ - `utils/id-utils.ts`: Id normalization helpers (string/ObjectId) and ref field coercion.
15
+
16
+ ## Quick Start
17
+
18
+ ```ts
19
+ import { MongoClient } from "mongodb";
20
+ import { z } from "zod";
21
+ import { createMongoDbContext } from "@appflare/server/db";
22
+
23
+ const client = await MongoClient.connect(process.env.MONGO_URL!);
24
+ const db = client.db("appflare-demo");
25
+
26
+ const schema = {
27
+ users: z.object({
28
+ _id: z.string(),
29
+ _creationTime: z.number(),
30
+ email: z.string(),
31
+ // ref:tickets tells the system this field references the tickets table
32
+ tickets: z.array(z.string().describe("ref:tickets")).optional(),
33
+ }),
34
+ tickets: z.object({
35
+ _id: z.string(),
36
+ _creationTime: z.number(),
37
+ title: z.string(),
38
+ user: z.string().describe("ref:users"),
39
+ }),
40
+ } as const;
41
+
42
+ const ctx = createMongoDbContext({ db, schema });
43
+
44
+ // typed table clients
45
+ const users = ctx.users;
46
+ const tickets = ctx.tickets;
47
+
48
+ // create
49
+ const user = await users.create({ data: { email: "a@demo.com", tickets: [] } });
50
+
51
+ // query with select + populate
52
+ const withTickets = await users.findUnique({
53
+ where: { _id: user._id },
54
+ select: ["_id", "email"],
55
+ include: ["tickets"],
56
+ });
57
+
58
+ // update many
59
+ await tickets.updateMany({
60
+ where: { user: user._id },
61
+ data: { title: "Updated" },
62
+ });
63
+ ```
64
+
65
+ ## Core Concepts
66
+
67
+ - **Context**: `createMongoDbContext` wires a MongoDB `Db`, a Zod schema map, and optional collection naming into typed table clients. Each table client wraps insert/update/delete/query logic and handles reference normalization.
68
+ - **Reference inference**: `buildSchemaRefMap` inspects Zod field descriptions that start with `ref:` to discover forward references. This enables automatic population and reference normalization.
69
+ - **Id normalization**: `normalizeIdValue` coerces valid string ids into `ObjectId` for filters and writes; `stringifyIdField` converts `_id` back to hex string on reads. Ref fields are normalized similarly via `normalizeRefFields`/`stringifyRefFields`.
70
+ - **Populate**: `populate()` on a query triggers `applyPopulate`, which performs `$lookup` pipelines. Forward populate (table stores the ref) is preferred; if absent, reverse populate finds documents that reference the current table (e.g., `tickets.user -> users._id`).
71
+ - **Typed chaining**: Query builders keep result types in sync when `select` or `populate` is used. Update/delete builders offer a fluent `where(...).set(...).exec()` style when called with one argument.
72
+
73
+ ## API Reference
74
+
75
+ ### Context Factory
76
+
77
+ - `createMongoDbContext(options)`:
78
+ - `db`: MongoDB `Db` instance.
79
+ - `schema`: Record of Zod schemas for each table (must include `_id` and `_creationTime`).
80
+ - `collectionName?`: Optional mapper `(tableName) => collectionName`.
81
+ - Returns `MongoDbContext` — a map of table names to `AppflareTableClient`.
82
+
83
+ ### Table Client (`AppflareTableClient`)
84
+
85
+ Methods match Prisma-like signatures:
86
+
87
+ - `findMany({ where?, orderBy?, skip?, take?, select?, include? })`
88
+ - `findFirst({ ... })`
89
+ - `findUnique({ where, select?, include? })`
90
+ - `create({ data, select?, include? })`
91
+ - `update({ where, data, select?, include? })`
92
+ - `updateMany({ where?, data })`
93
+ - `delete({ where, select?, include? })`
94
+ - `deleteMany({ where? })`
95
+ - `count({ where? })`
96
+
97
+ `select` accepts an array or object of field keys; `include` accepts populatable relation keys. Both adjust the result type.
98
+
99
+ ### Query Builder
100
+
101
+ Produced via `ctx.<table>.findMany()` under the hood and exposed through `core.query()`:
102
+
103
+ - `where(filter)`: chainable; multiple calls AND together.
104
+ - `sort(sortSpec)`: accepts object or tuples; `desc` maps to `-1` for Mongo.
105
+ - `limit(n)` / `offset(n)`
106
+ - `select(...keys)`
107
+ - `populate(key | keys[])`
108
+ - `find()` returns an array; `findOne()` returns first or `null`.
109
+
110
+ ### Update/Patch/Delete Builders
111
+
112
+ When `update`, `patch`, or `delete` are called with only the table name, they return a builder:
113
+
114
+ ```ts
115
+ await ctx.users
116
+ .update("users")
117
+ .where({ email: /@demo/ })
118
+ .set({ email: "x" })
119
+ .exec();
120
+ await ctx.users.delete("users").where("someId").exec();
121
+ ```
122
+
123
+ `patch` is an alias of `update`.
124
+
125
+ ### Populate Behavior
126
+
127
+ - Forward populate uses `$lookup` from the current collection to the referenced table (`localField` = ref field, `foreignField` = `_id`). Arrays are matched element-wise.
128
+ - Reverse populate triggers when the current table lacks the ref but others point to it. It looks up documents in the referencing table and groups them by current `_id`.
129
+ - Populate respects `select`: projection is expanded to include ref keys so lookups have ids even when omitted from the requested fields.
130
+
131
+ ### Utilities
132
+
133
+ - `buildProjection(keys)`: builds a Mongo projection, ensuring `_id` and `_creationTime` are excluded when not selected.
134
+ - `normalizeSort(sort)`: converts sort objects/tuples to Mongo format.
135
+ - `normalizeIdFilter`, `normalizeRefFields`, `stringifyIdField`: ensure consistent id types across reads/writes.
136
+
137
+ ## Usage Notes
138
+
139
+ - Always include `_id` and `_creationTime` in your Zod schema; they are used by the helpers and default projections.
140
+ - Reference discovery relies on `describe("ref:<table>")` on string fields (optionally inside arrays/optional/nullable/default wrappers).
141
+ - All writes normalize ids to `ObjectId` when valid; reads stringify `_id` for consumer-friendly output.
142
+ - `findUnique` requires a `where` clause; `findFirst` defaults `take` to `1` when unset.
143
+
144
+ ## Extending
145
+
146
+ - Supply a custom `collectionName` to map logical table names to physical collections.
147
+ - Customize partial normalization by passing `normalizePartial` to `createUpdateBuilder`/`createPatchBuilder` if you wrap the builders yourself.
148
+
149
+ ## Related Files
150
+
151
+ - Types: `types/types.ts`, `types/schema-refs.ts`
152
+ - Database helpers: `database/context.ts`, `database/query-builder.ts`, `database/builders.ts`, `database/populate.ts`, `database/query-utils.ts`
153
+ - Id helpers: `utils/id-utils.ts`
@@ -0,0 +1,83 @@
1
+ import type { Collection, Document } from "mongodb";
2
+ import { isIdValue, normalizeIdFilter, toMongoFilter } from "../utils/id-utils";
3
+ import type {
4
+ MongoDbDeleteBuilder,
5
+ MongoDbPatchBuilder,
6
+ MongoDbUpdateBuilder,
7
+ } from "../types/types";
8
+
9
+ export function createDeleteBuilder(params: {
10
+ table: string;
11
+ getCollection: (table: string) => Collection<Document>;
12
+ }): MongoDbDeleteBuilder<any, any> {
13
+ return {
14
+ where(where) {
15
+ const filter = normalizeIdFilter(toMongoFilter(where));
16
+ return {
17
+ exec: async () => {
18
+ const coll = params.getCollection(params.table);
19
+ if (isIdValue(where)) {
20
+ await coll.deleteOne(filter as any);
21
+ } else {
22
+ await coll.deleteMany(filter as any);
23
+ }
24
+ },
25
+ };
26
+ },
27
+ };
28
+ }
29
+
30
+ export function createUpdateBuilder(params: {
31
+ table: string;
32
+ getCollection: (table: string) => Collection<Document>;
33
+ normalizePartial?: (
34
+ partial: Record<string, unknown>
35
+ ) => Record<string, unknown>;
36
+ }): MongoDbUpdateBuilder<any, any> {
37
+ return {
38
+ where(where) {
39
+ const filter = normalizeIdFilter(toMongoFilter(where));
40
+ return {
41
+ set(partial) {
42
+ const normalized = params.normalizePartial
43
+ ? params.normalizePartial(partial as any)
44
+ : partial;
45
+ return {
46
+ exec: async () => {
47
+ const coll = params.getCollection(params.table);
48
+ const update = { $set: normalized as any };
49
+ if (isIdValue(where)) {
50
+ await coll.updateOne(filter as any, update);
51
+ } else {
52
+ await coll.updateMany(filter as any, update);
53
+ }
54
+ },
55
+ };
56
+ },
57
+ exec: async (partial) => {
58
+ if (!partial) throw new Error("update requires a partial to set");
59
+ const normalized = params.normalizePartial
60
+ ? params.normalizePartial(partial as any)
61
+ : partial;
62
+ const coll = params.getCollection(params.table);
63
+ const update = { $set: normalized as any };
64
+ if (isIdValue(where)) {
65
+ await coll.updateOne(filter as any, update);
66
+ } else {
67
+ await coll.updateMany(filter as any, update);
68
+ }
69
+ },
70
+ };
71
+ },
72
+ };
73
+ }
74
+
75
+ export function createPatchBuilder(params: {
76
+ table: string;
77
+ getCollection: (table: string) => Collection<Document>;
78
+ normalizePartial?: (
79
+ partial: Record<string, unknown>
80
+ ) => Record<string, unknown>;
81
+ }): MongoDbPatchBuilder<any, any> {
82
+ return createUpdateBuilder(params) as MongoDbPatchBuilder<any, any>;
83
+ }
@@ -0,0 +1,265 @@
1
+ import type { Collection, Document } from "mongodb";
2
+ import { ObjectId } from "mongodb";
3
+ import {
4
+ createDeleteBuilder,
5
+ createPatchBuilder,
6
+ createUpdateBuilder,
7
+ } from "./builders";
8
+ import { createQueryBuilder } from "./query-builder";
9
+ import {
10
+ isIdValue,
11
+ normalizeIdFilter,
12
+ normalizeRefFields,
13
+ toMongoFilter,
14
+ } from "../utils/id-utils";
15
+ import { buildSchemaRefMap } from "../types/schema-refs";
16
+ import type {
17
+ CreateMongoDbContextOptions,
18
+ MongoDbCoreContext,
19
+ MongoDbContext,
20
+ MongoDbQuery,
21
+ AppflareTableClient,
22
+ TableDocBase,
23
+ } from "../types/types";
24
+
25
+ export function createMongoDbContext<
26
+ TTableNames extends string,
27
+ TTableDocMap extends Record<TTableNames, TableDocBase>,
28
+ >(
29
+ options: CreateMongoDbContextOptions<TTableNames>
30
+ ): MongoDbContext<TTableNames, TTableDocMap> {
31
+ const collectionName = options.collectionName ?? ((t) => t);
32
+ const collections = new Map<string, Collection<Document>>();
33
+ const refs = buildSchemaRefMap(options.schema);
34
+ const tableNames = Object.keys(options.schema) as TTableNames[];
35
+
36
+ const getCollection = (table: string): Collection<Document> => {
37
+ const key = collectionName(table as any);
38
+ const existing = collections.get(key);
39
+ if (existing) return existing;
40
+ const created = options.db.collection(key);
41
+ collections.set(key, created);
42
+ return created;
43
+ };
44
+
45
+ const core: MongoDbCoreContext<TTableNames, TTableDocMap> = {
46
+ query: (table) =>
47
+ createQueryBuilder<TTableNames, TTableDocMap, any>({
48
+ table: table as any,
49
+ getCollection,
50
+ refs,
51
+ }),
52
+ insert: async (table, value) => {
53
+ const coll = getCollection(table as string);
54
+ const objectId = new ObjectId();
55
+ const normalized = normalizeRefFields(
56
+ table as string,
57
+ value as Record<string, unknown>,
58
+ refs
59
+ );
60
+ const doc = {
61
+ ...normalized,
62
+ _id: objectId,
63
+ _creationTime: Date.now(),
64
+ } as unknown as Document;
65
+
66
+ await coll.insertOne(doc);
67
+ return objectId.toHexString() as any;
68
+ },
69
+ update: ((table: any, where?: any, partial?: any) => {
70
+ if (arguments.length === 1) {
71
+ return createUpdateBuilder({
72
+ table,
73
+ getCollection,
74
+ normalizePartial: (p) =>
75
+ normalizeRefFields(table as string, p as any, refs),
76
+ });
77
+ }
78
+ const coll = getCollection(table as string);
79
+ const filter = normalizeIdFilter(toMongoFilter(where));
80
+ const update = {
81
+ $set: normalizeRefFields(table as string, partial as any, refs) as any,
82
+ };
83
+ if (isIdValue(where)) {
84
+ return coll.updateOne(filter, update).then(() => {});
85
+ }
86
+ return coll.updateMany(filter, update).then(() => {});
87
+ }) as any,
88
+ patch: ((table: any, where?: any, partial?: any) => {
89
+ if (arguments.length === 1) {
90
+ return createPatchBuilder({
91
+ table,
92
+ getCollection,
93
+ normalizePartial: (p) =>
94
+ normalizeRefFields(table as string, p as any, refs),
95
+ });
96
+ }
97
+ const coll = getCollection(table as string);
98
+ const filter = normalizeIdFilter(toMongoFilter(where));
99
+ const update = {
100
+ $set: normalizeRefFields(table as string, partial as any, refs) as any,
101
+ };
102
+ if (isIdValue(where)) {
103
+ return coll.updateOne(filter, update).then(() => {});
104
+ }
105
+ return coll.updateMany(filter, update).then(() => {});
106
+ }) as any,
107
+ delete: ((table: any, where?: any) => {
108
+ if (arguments.length === 1) {
109
+ return createDeleteBuilder({
110
+ table,
111
+ getCollection,
112
+ });
113
+ }
114
+ const coll = getCollection(table as string);
115
+ const filter = normalizeIdFilter(toMongoFilter(where));
116
+ if (isIdValue(where)) {
117
+ return coll.deleteOne(filter).then(() => {});
118
+ }
119
+ return coll.deleteMany(filter).then(() => {});
120
+ }) as any,
121
+ };
122
+
123
+ const ctx = {} as MongoDbContext<TTableNames, TTableDocMap>;
124
+
125
+ for (const table of tableNames) {
126
+ (ctx as any)[table] = createAppflareTableClient({
127
+ table: table as TTableNames,
128
+ core,
129
+ refs,
130
+ getCollection,
131
+ });
132
+ }
133
+
134
+ return ctx;
135
+ }
136
+
137
+ function toKeyList(arg: unknown): string[] | undefined {
138
+ if (!arg) return undefined;
139
+ if (Array.isArray(arg)) return arg.map((k) => String(k));
140
+ if (typeof arg === "object") {
141
+ return Object.entries(arg as Record<string, unknown>)
142
+ .filter(([, v]) => Boolean(v))
143
+ .map(([k]) => k);
144
+ }
145
+ return undefined;
146
+ }
147
+
148
+ function createAppflareTableClient<
149
+ TTableNames extends string,
150
+ TTableDocMap extends Record<TTableNames, TableDocBase>,
151
+ TableName extends TTableNames,
152
+ >(params: {
153
+ table: TableName;
154
+ core: MongoDbCoreContext<TTableNames, TTableDocMap>;
155
+ refs: ReturnType<typeof buildSchemaRefMap>;
156
+ getCollection: (table: string) => Collection<Document>;
157
+ }): AppflareTableClient<TableName, TTableDocMap> {
158
+ const selectKeys = (select: unknown) => toKeyList(select);
159
+ const includeKeys = (include: unknown) => toKeyList(include);
160
+
161
+ const buildQuery = (args?: {
162
+ where?: any;
163
+ orderBy?: any;
164
+ skip?: number;
165
+ take?: number;
166
+ select?: unknown;
167
+ include?: unknown;
168
+ }) => {
169
+ let q: MongoDbQuery<TableName, TTableDocMap, any> = params.core.query(
170
+ params.table as any
171
+ ) as any;
172
+ if (args?.where) q = q.where(args.where as any);
173
+ if (args?.orderBy) q = q.sort(args.orderBy as any);
174
+ if (args?.skip !== undefined) q = q.offset(args.skip);
175
+ if (args?.take !== undefined) q = q.limit(args.take);
176
+ const s = selectKeys(args?.select);
177
+ if (s?.length) q = q.select(s as any);
178
+ const i = includeKeys(args?.include);
179
+ if (i?.length) q = q.populate(i as any);
180
+ return q;
181
+ };
182
+
183
+ const fetchOne = async (args: {
184
+ where: any;
185
+ select?: unknown;
186
+ include?: unknown;
187
+ }) => {
188
+ const q = buildQuery({ ...args, take: 1 });
189
+ return q.findOne();
190
+ };
191
+
192
+ return {
193
+ findMany: async (args) => buildQuery(args as any).find() as any,
194
+ findFirst: async (args) =>
195
+ buildQuery({
196
+ ...(args as any),
197
+ take: (args as any)?.take ?? 1,
198
+ }).findOne() as any,
199
+ findUnique: async (args) => {
200
+ if (!args?.where) throw new Error("findUnique requires a where clause");
201
+ return fetchOne(args as any) as any;
202
+ },
203
+ create: async (args) => {
204
+ const id = await params.core.insert(
205
+ params.table as any,
206
+ args.data as any
207
+ );
208
+ const created = await fetchOne({
209
+ where: { _id: id } as any,
210
+ select: (args as any)?.select,
211
+ include: (args as any)?.include,
212
+ });
213
+ return (created ?? {
214
+ _id: id,
215
+ _creationTime: Date.now(),
216
+ ...args.data,
217
+ }) as any;
218
+ },
219
+ update: async (args) => {
220
+ await params.core.update(
221
+ params.table as any,
222
+ args.where as any,
223
+ args.data as any
224
+ );
225
+ return fetchOne({
226
+ where: args.where as any,
227
+ select: (args as any)?.select,
228
+ include: (args as any)?.include,
229
+ }) as any;
230
+ },
231
+ updateMany: async (args) => {
232
+ const coll = params.getCollection(params.table as string);
233
+ const filter = normalizeIdFilter(toMongoFilter(args.where ?? {}));
234
+ const normalized = normalizeRefFields(
235
+ params.table as string,
236
+ args.data as any,
237
+ params.refs
238
+ );
239
+ const result = await coll.updateMany(filter as any, {
240
+ $set: normalized as any,
241
+ });
242
+ return { count: result.modifiedCount ?? 0 };
243
+ },
244
+ delete: async (args) => {
245
+ const existing = await fetchOne({
246
+ where: args.where as any,
247
+ select: (args as any)?.select,
248
+ include: (args as any)?.include,
249
+ });
250
+ await params.core.delete(params.table as any, args.where as any);
251
+ return existing as any;
252
+ },
253
+ deleteMany: async (args) => {
254
+ const coll = params.getCollection(params.table as string);
255
+ const filter = normalizeIdFilter(toMongoFilter(args?.where ?? {}));
256
+ const result = await coll.deleteMany(filter as any);
257
+ return { count: result.deletedCount ?? 0 };
258
+ },
259
+ count: async (args) => {
260
+ const coll = params.getCollection(params.table as string);
261
+ const filter = normalizeIdFilter(toMongoFilter(args?.where ?? {}));
262
+ return coll.countDocuments(filter ?? {});
263
+ },
264
+ };
265
+ }