@takosjp/yurucommu-core 4.0.0 → 4.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/README.en.md +3 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/packages/api/package.json +1 -1
- package/src/backend/index.ts +44 -16
- package/src/backend/public.ts +60 -0
- package/src/backend/routes/media.ts +4 -0
- package/src/backend/runtime/edge-facades.ts +336 -0
- package/src/backend/runtime/edge-kv.ts +204 -0
- package/src/backend/runtime/edge-objects.ts +120 -0
- package/src/backend/runtime/edge-queue.ts +135 -0
- package/src/backend/runtime/edge-sql.ts +207 -0
- package/src/backend/runtime/lane.ts +291 -0
- package/src/backend/runtime/managed-relational.ts +70 -23
- package/src/backend/runtime/sqlite-proxy-rows.ts +309 -0
- package/src/backend/server.ts +5 -0
- package/src/backend/types.ts +9 -0
- package/src/db/index.ts +2 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which runtime the Worker was published onto, and the bindings that follow.
|
|
3
|
+
*
|
|
4
|
+
* The same bundle runs on two backends that look identical from inside:
|
|
5
|
+
*
|
|
6
|
+
* `cloudflare` — RAW Cloudflare bindings. `env.DB` is a `D1Database`,
|
|
7
|
+
* `env.KV` a `KVNamespace`, `env.MEDIA` an `R2Bucket`,
|
|
8
|
+
* `env.DELIVERY_QUEUE` a `Queue`. This is a Worker deployed
|
|
9
|
+
* straight to Cloudflare, and equally an ordinary-Workers
|
|
10
|
+
* Takoserver backend, which projects those same raw bindings.
|
|
11
|
+
* `portable` — the PORTABLE FACADES. A wrapper host — a self-hosted
|
|
12
|
+
* Takoserver, or a managed Workers-for-Platforms backend —
|
|
13
|
+
* replaces `env` before the module sees it, and each binding
|
|
14
|
+
* is the facade its Interface names: `edge.sql`, `edge.kv`,
|
|
15
|
+
* `edge.objects`, `edge.queue`.
|
|
16
|
+
*
|
|
17
|
+
* THE LANE NAMES THE BINDING SHAPE, not the tool that published the Worker. A
|
|
18
|
+
* deployment authored in Takoform lands on either one depending on the host it
|
|
19
|
+
* targets, so the lane cannot be inferred from the IaC that produced it.
|
|
20
|
+
*
|
|
21
|
+
* THE LANE IS DECLARED, NOT SNIFFED. Two of the bindings cannot be told apart
|
|
22
|
+
* by shape at all — `edge.kv` and `KVNamespace` expose the same five method
|
|
23
|
+
* names, and both queue producers are `send`/`sendBatch`. A Worker that guessed
|
|
24
|
+
* would call `kv.get(key, {type:"json"})` on a facade that ignores the second
|
|
25
|
+
* argument and returns bytes, and the failure would surface much later as a
|
|
26
|
+
* corrupt session or a rate-limit that never trips.
|
|
27
|
+
*
|
|
28
|
+
* So the lane comes from `YURUCOMMU_RUNTIME_LANE`, which a self-host or managed
|
|
29
|
+
* Workers-for-Platforms deployment sets to `portable` and every raw-binding
|
|
30
|
+
* deployment leaves unset (or `cloudflare`). The declaration is then
|
|
31
|
+
* cross-checked against the bindings that ARE decisive — `DB` always, `MEDIA`
|
|
32
|
+
* when it is bound. A disagreement refuses to start.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type {
|
|
36
|
+
D1Database,
|
|
37
|
+
Fetcher,
|
|
38
|
+
KVNamespace,
|
|
39
|
+
MessageBatch,
|
|
40
|
+
Queue,
|
|
41
|
+
R2Bucket,
|
|
42
|
+
} from "@cloudflare/workers-types";
|
|
43
|
+
|
|
44
|
+
import type { Database } from "../../db/index.ts";
|
|
45
|
+
import {
|
|
46
|
+
isEdgeObjectsBinding,
|
|
47
|
+
isEdgeQueueBatch,
|
|
48
|
+
isEdgeSqlBinding,
|
|
49
|
+
isNativeD1Database,
|
|
50
|
+
isNativeR2Bucket,
|
|
51
|
+
type EdgeKvBinding,
|
|
52
|
+
type EdgeObjectsBinding,
|
|
53
|
+
type EdgeQueueBatch,
|
|
54
|
+
type EdgeQueueBinding,
|
|
55
|
+
type EdgeSqlBinding,
|
|
56
|
+
} from "./edge-facades.ts";
|
|
57
|
+
import { createEdgeSqlDatabase } from "./edge-sql.ts";
|
|
58
|
+
import { wrapEdgeKv } from "./edge-kv.ts";
|
|
59
|
+
import { wrapEdgeMessageBatch, wrapEdgeQueue } from "./edge-queue.ts";
|
|
60
|
+
import { wrapEdgeObjects } from "./edge-objects.ts";
|
|
61
|
+
import {
|
|
62
|
+
wrapCloudflareBindings,
|
|
63
|
+
wrapCloudflareMessageBatch,
|
|
64
|
+
} from "./cloudflare.ts";
|
|
65
|
+
import type { IKeyValueStore, IStaticAssets, ObjectStore } from "./types.ts";
|
|
66
|
+
import type { IQueueBatch, IQueueProducer } from "./queue.ts";
|
|
67
|
+
|
|
68
|
+
/** The variable that names the lane. Set it in the deployment's plain vars. */
|
|
69
|
+
export const RUNTIME_LANE_VAR = "YURUCOMMU_RUNTIME_LANE";
|
|
70
|
+
|
|
71
|
+
/** Every lane this build knows how to run on. */
|
|
72
|
+
export const RUNTIME_LANES = ["cloudflare", "portable"] as const;
|
|
73
|
+
|
|
74
|
+
export type RuntimeLane = (typeof RUNTIME_LANES)[number];
|
|
75
|
+
|
|
76
|
+
/** The lane when the variable is absent: a plain Cloudflare Worker. */
|
|
77
|
+
export const DEFAULT_RUNTIME_LANE: RuntimeLane = "cloudflare";
|
|
78
|
+
|
|
79
|
+
/** The declared lane is unknown, or disagrees with the bindings that arrived. */
|
|
80
|
+
export class RuntimeLaneError extends Error {
|
|
81
|
+
constructor(message: string) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.name = "RuntimeLaneError";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read the declared lane.
|
|
89
|
+
*
|
|
90
|
+
* An unset variable is the Cloudflare lane, because that is what a Worker
|
|
91
|
+
* deployed without Takoform is. An UNRECOGNISED value is refused rather than
|
|
92
|
+
* defaulted: a future Host that names a lane this build has never heard of must
|
|
93
|
+
* not be served by guessing that its bindings are Cloudflare's.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveRuntimeLane(declared: unknown): RuntimeLane {
|
|
96
|
+
if (declared === undefined || declared === null || declared === "") {
|
|
97
|
+
return DEFAULT_RUNTIME_LANE;
|
|
98
|
+
}
|
|
99
|
+
if (typeof declared !== "string") {
|
|
100
|
+
throw new RuntimeLaneError(
|
|
101
|
+
`${RUNTIME_LANE_VAR} must be a string; received ${typeof declared}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const lane = declared.trim();
|
|
105
|
+
if ((RUNTIME_LANES as readonly string[]).includes(lane)) {
|
|
106
|
+
return lane as RuntimeLane;
|
|
107
|
+
}
|
|
108
|
+
throw new RuntimeLaneError(
|
|
109
|
+
`${RUNTIME_LANE_VAR}="${declared}" is not a runtime lane this build ` +
|
|
110
|
+
`supports (${RUNTIME_LANES.join(", ")}). Refusing to start rather than ` +
|
|
111
|
+
`assume a binding shape.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface LaneBindings {
|
|
116
|
+
readonly DB?: unknown;
|
|
117
|
+
readonly MEDIA?: unknown;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Prove the declared lane against the bindings that can actually be identified.
|
|
122
|
+
*
|
|
123
|
+
* `DB` is always decisive: `execute`/`query`/`transaction` and
|
|
124
|
+
* `prepare`/`batch` are disjoint. `MEDIA` is decisive only in one direction —
|
|
125
|
+
* an `R2Bucket` is recognisable by its multipart helpers, whereas a plain
|
|
126
|
+
* five-method object could be the facade or an adapter a host repository
|
|
127
|
+
* supplied — so only the direction that can be proven is checked.
|
|
128
|
+
*/
|
|
129
|
+
export function assertRuntimeLaneBindings(
|
|
130
|
+
lane: RuntimeLane,
|
|
131
|
+
bindings: LaneBindings,
|
|
132
|
+
): void {
|
|
133
|
+
const { DB, MEDIA } = bindings;
|
|
134
|
+
if (lane === "portable") {
|
|
135
|
+
if (isNativeD1Database(DB)) {
|
|
136
|
+
throw new RuntimeLaneError(
|
|
137
|
+
`${RUNTIME_LANE_VAR}="portable" declares the portable-facade lane, ` +
|
|
138
|
+
`but env.DB is a native D1Database (prepare/batch). A host that ` +
|
|
139
|
+
`projects raw Cloudflare bindings — including an ordinary-Workers ` +
|
|
140
|
+
`Takoserver backend — is the cloudflare lane; leave the variable ` +
|
|
141
|
+
`unset there.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (!isEdgeSqlBinding(DB)) {
|
|
145
|
+
throw new RuntimeLaneError(
|
|
146
|
+
`${RUNTIME_LANE_VAR}="portable" requires env.DB to be the ` +
|
|
147
|
+
`edge.sql@1.0.0 facade (execute/query/transaction); it exposes ` +
|
|
148
|
+
`neither that nor D1's prepare/batch.`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (MEDIA !== undefined && isNativeR2Bucket(MEDIA)) {
|
|
152
|
+
throw new RuntimeLaneError(
|
|
153
|
+
`${RUNTIME_LANE_VAR}="portable" declares the portable-facade lane, ` +
|
|
154
|
+
`but env.MEDIA is a native R2Bucket. A portable bucket binding ` +
|
|
155
|
+
`arrives as the edge.objects@1.0.0 facade.`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (isEdgeSqlBinding(DB)) {
|
|
161
|
+
throw new RuntimeLaneError(
|
|
162
|
+
`env.DB is the edge.sql@1.0.0 facade (execute/query/transaction), but ` +
|
|
163
|
+
`${RUNTIME_LANE_VAR} does not declare the portable lane. A Worker on a ` +
|
|
164
|
+
`wrapper host must declare it; without that this build would hand the ` +
|
|
165
|
+
`facade to drizzle-orm/d1 and every query would fail at the first ` +
|
|
166
|
+
`prepare().`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (!isNativeD1Database(DB)) {
|
|
170
|
+
throw new RuntimeLaneError(
|
|
171
|
+
`env.DB is neither a D1Database nor the edge.sql@1.0.0 facade; the ` +
|
|
172
|
+
`Cloudflare lane cannot build a database client from it.`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Bindings a Worker receives from a host that projects portable facades. */
|
|
178
|
+
export interface PortableWorkerBindings {
|
|
179
|
+
DB: EdgeSqlBinding;
|
|
180
|
+
KV: EdgeKvBinding;
|
|
181
|
+
MEDIA?: EdgeObjectsBinding;
|
|
182
|
+
ASSETS?: IStaticAssets;
|
|
183
|
+
DELIVERY_QUEUE?: EdgeQueueBinding;
|
|
184
|
+
DELIVERY_DLQ?: EdgeQueueBinding;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Bindings a Worker deployed straight to Cloudflare receives. */
|
|
188
|
+
export interface CloudflareWorkerBindings {
|
|
189
|
+
DB: D1Database;
|
|
190
|
+
KV: KVNamespace;
|
|
191
|
+
MEDIA?: R2Bucket;
|
|
192
|
+
ASSETS?: Fetcher;
|
|
193
|
+
DELIVERY_QUEUE?: Queue<unknown>;
|
|
194
|
+
DELIVERY_DLQ?: Queue<unknown>;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
type WrappedRuntime<T> = Omit<
|
|
198
|
+
T,
|
|
199
|
+
"DB" | "MEDIA" | "KV" | "ASSETS" | "DELIVERY_QUEUE" | "DELIVERY_DLQ"
|
|
200
|
+
> & {
|
|
201
|
+
DB_INSTANCE: Database;
|
|
202
|
+
MEDIA?: ObjectStore;
|
|
203
|
+
KV: IKeyValueStore;
|
|
204
|
+
ASSETS?: IStaticAssets;
|
|
205
|
+
DELIVERY_QUEUE?: IQueueProducer<unknown>;
|
|
206
|
+
DELIVERY_DLQ?: IQueueProducer<unknown>;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Wrap the portable facades into the runtime ports the app speaks.
|
|
211
|
+
*
|
|
212
|
+
* `ASSETS` passes through: a Takoform `external_services` entry is projected as
|
|
213
|
+
* a `{fetch}` adapter, which is already the port's whole surface.
|
|
214
|
+
*/
|
|
215
|
+
export function wrapPortableBindings<T extends PortableWorkerBindings>(
|
|
216
|
+
bindings: T,
|
|
217
|
+
): WrappedRuntime<T> {
|
|
218
|
+
const { DB, MEDIA, KV, ASSETS, DELIVERY_QUEUE, DELIVERY_DLQ, ...rest } =
|
|
219
|
+
bindings;
|
|
220
|
+
return {
|
|
221
|
+
...rest,
|
|
222
|
+
DB_INSTANCE: createEdgeSqlDatabase(DB),
|
|
223
|
+
MEDIA: MEDIA ? wrapEdgeObjects(MEDIA) : undefined,
|
|
224
|
+
KV: wrapEdgeKv(KV),
|
|
225
|
+
ASSETS,
|
|
226
|
+
DELIVERY_QUEUE: DELIVERY_QUEUE ? wrapEdgeQueue(DELIVERY_QUEUE) : undefined,
|
|
227
|
+
DELIVERY_DLQ: DELIVERY_DLQ ? wrapEdgeQueue(DELIVERY_DLQ) : undefined,
|
|
228
|
+
} as unknown as WrappedRuntime<T>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The single entry point a Worker should call.
|
|
233
|
+
*
|
|
234
|
+
* Reads {@link RUNTIME_LANE_VAR} off the bindings themselves — on both lanes it
|
|
235
|
+
* is an ordinary plain-text variable that arrives alongside them — proves the
|
|
236
|
+
* lane against the decisive bindings, and then wraps.
|
|
237
|
+
*/
|
|
238
|
+
export function wrapRuntimeBindings<
|
|
239
|
+
// Deliberately structural. Which of the two binding sets this actually is,
|
|
240
|
+
// is the runtime question this function answers; a static union here would
|
|
241
|
+
// only force every caller to assert the answer before asking it.
|
|
242
|
+
T extends { DB: unknown; KV: unknown },
|
|
243
|
+
>(bindings: T): WrappedRuntime<T> {
|
|
244
|
+
const lane = resolveRuntimeLane(
|
|
245
|
+
(bindings as Record<string, unknown>)[RUNTIME_LANE_VAR],
|
|
246
|
+
);
|
|
247
|
+
assertRuntimeLaneBindings(lane, bindings as LaneBindings);
|
|
248
|
+
return lane === "portable"
|
|
249
|
+
? (wrapPortableBindings(
|
|
250
|
+
bindings as unknown as PortableWorkerBindings,
|
|
251
|
+
) as unknown as WrappedRuntime<T>)
|
|
252
|
+
: (wrapCloudflareBindings(
|
|
253
|
+
bindings as unknown as CloudflareWorkerBindings & {
|
|
254
|
+
DB: D1Database;
|
|
255
|
+
KV: KVNamespace;
|
|
256
|
+
},
|
|
257
|
+
) as unknown as WrappedRuntime<T>);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Adapt one consumer batch for whichever lane produced it.
|
|
262
|
+
*
|
|
263
|
+
* A queue batch IS decisive — the facade settles with `acknowledgeAll`, the
|
|
264
|
+
* Cloudflare `MessageBatch` with `ackAll` — so the shape is checked against the
|
|
265
|
+
* declared lane rather than trusted on its own.
|
|
266
|
+
*/
|
|
267
|
+
export function wrapRuntimeMessageBatch<T>(
|
|
268
|
+
batch: MessageBatch<T> | EdgeQueueBatch,
|
|
269
|
+
lane: RuntimeLane = DEFAULT_RUNTIME_LANE,
|
|
270
|
+
): IQueueBatch<T> {
|
|
271
|
+
const isFacade = isEdgeQueueBatch(batch);
|
|
272
|
+
if (lane === "portable") {
|
|
273
|
+
if (!isFacade) {
|
|
274
|
+
throw new RuntimeLaneError(
|
|
275
|
+
`${RUNTIME_LANE_VAR}="portable" declares the portable-facade lane, ` +
|
|
276
|
+
`but the queue event is a Cloudflare MessageBatch (ackAll).`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return wrapEdgeMessageBatch<T>(batch);
|
|
280
|
+
}
|
|
281
|
+
if (isFacade) {
|
|
282
|
+
throw new RuntimeLaneError(
|
|
283
|
+
`The queue event is a portable-facade batch (acknowledgeAll), but ` +
|
|
284
|
+
`${RUNTIME_LANE_VAR} does not declare the portable lane.`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return wrapCloudflareMessageBatch(batch as MessageBatch<T>);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Re-exported so a Worker entry can probe MEDIA without importing internals. */
|
|
291
|
+
export { isEdgeObjectsBinding };
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
parseManagedRelationalBatchResponse,
|
|
6
6
|
type ManagedRelationalMethod,
|
|
7
7
|
type ManagedRelationalParameter,
|
|
8
|
+
type ManagedRelationalStatement,
|
|
8
9
|
} from "@takosjp/takosumi-contract/managed-relational-runtime";
|
|
9
10
|
import { parseManagedRuntimeConnectionMaterialization } from "@takosjp/takosumi-contract/managed-runtime-connections";
|
|
10
11
|
import {
|
|
@@ -16,9 +17,17 @@ import {
|
|
|
16
17
|
import * as schema from "../../db/schema.ts";
|
|
17
18
|
import { ManagedRuntimeGatewayError } from "./managed-runtime.ts";
|
|
18
19
|
import type { ManagedRuntimeGateway } from "./managed-runtime.ts";
|
|
20
|
+
import {
|
|
21
|
+
positionalRow,
|
|
22
|
+
rewriteProjection,
|
|
23
|
+
type ProjectedStatement,
|
|
24
|
+
} from "./sqlite-proxy-rows.ts";
|
|
19
25
|
|
|
20
26
|
const DEFAULT_MAX_RELATIONAL_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
21
27
|
|
|
28
|
+
/** The lane name a row-shape refusal reports. */
|
|
29
|
+
const LANE = "managed relational";
|
|
30
|
+
|
|
22
31
|
export interface ManagedRelationalDatabaseOptions {
|
|
23
32
|
readonly materialization: unknown;
|
|
24
33
|
readonly gateway: ManagedRuntimeGateway;
|
|
@@ -33,6 +42,11 @@ export interface ManagedRelationalDatabaseOptions {
|
|
|
33
42
|
* Every callback is one bounded prepared statement. Drizzle `batch()` maps to
|
|
34
43
|
* one ordered-atomic host call; transaction-control and migration SQL are
|
|
35
44
|
* deliberately unavailable on this request path.
|
|
45
|
+
*
|
|
46
|
+
* Row shape is shared with the `edge.sql` lane (`sqlite-proxy-rows.ts`): the
|
|
47
|
+
* projection list is rewritten so no two result columns share a name, the
|
|
48
|
+
* column count that comes back is checked against the count that went out, and
|
|
49
|
+
* the row Drizzle indexes positionally also answers to its column names.
|
|
36
50
|
*/
|
|
37
51
|
export function createManagedRelationalDatabase(
|
|
38
52
|
options: ManagedRelationalDatabaseOptions,
|
|
@@ -64,13 +78,9 @@ export function createManagedRelationalDatabase(
|
|
|
64
78
|
readonly method: ManagedRelationalMethod;
|
|
65
79
|
}[],
|
|
66
80
|
) => {
|
|
67
|
-
const
|
|
68
|
-
sql: statement.sql,
|
|
69
|
-
params: statement.params.map(relationalParameter),
|
|
70
|
-
method: statement.method,
|
|
71
|
-
}));
|
|
81
|
+
const prepared = statements.map(prepare);
|
|
72
82
|
const request = managedRelationalBatchGatewayRequest(connection.authority, {
|
|
73
|
-
statements:
|
|
83
|
+
statements: prepared.map((entry) => entry.statement),
|
|
74
84
|
idempotencyKey: idempotencyKey(),
|
|
75
85
|
});
|
|
76
86
|
const response = await boundedResponse(
|
|
@@ -91,40 +101,77 @@ export function createManagedRelationalDatabase(
|
|
|
91
101
|
}
|
|
92
102
|
const value = parseManagedRelationalBatchResponse(
|
|
93
103
|
await response.json(),
|
|
94
|
-
|
|
104
|
+
prepared.length,
|
|
105
|
+
);
|
|
106
|
+
return value.results.map((result, index) =>
|
|
107
|
+
drizzleResult(result, prepared[index]!),
|
|
95
108
|
);
|
|
96
|
-
return value.results;
|
|
97
109
|
};
|
|
98
110
|
|
|
99
111
|
const callback: AsyncRemoteCallback = async (sql, params, method) => {
|
|
100
112
|
const [result] = await execute([{ sql, params, method }]);
|
|
101
113
|
if (!result) throw new Error("managed_relational_result_missing");
|
|
102
|
-
return
|
|
103
|
-
};
|
|
104
|
-
const batchCallback: AsyncBatchRemoteCallback = async (batch) => {
|
|
105
|
-
const results = await execute(batch);
|
|
106
|
-
return results.map((result, index) =>
|
|
107
|
-
drizzleResult(result, batch[index]!.method),
|
|
108
|
-
);
|
|
114
|
+
return result;
|
|
109
115
|
};
|
|
116
|
+
const batchCallback: AsyncBatchRemoteCallback = async (batch) =>
|
|
117
|
+
await execute(batch);
|
|
110
118
|
|
|
111
119
|
return drizzleProxy(callback, batchCallback, { schema });
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
interface PreparedStatement {
|
|
123
|
+
readonly statement: ManagedRelationalStatement;
|
|
124
|
+
readonly projection: ProjectedStatement;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Trim first, then give every projected column a distinct name.
|
|
129
|
+
*
|
|
130
|
+
* The runtime contract refuses a statement whose text is not already trimmed,
|
|
131
|
+
* and Drizzle does not trim the raw `sql` template a call site wrote across
|
|
132
|
+
* several lines — so the personal block and mute gates' own statements would
|
|
133
|
+
* never reach the host at all.
|
|
134
|
+
*/
|
|
135
|
+
function prepare(statement: {
|
|
136
|
+
readonly sql: string;
|
|
137
|
+
readonly params: readonly unknown[];
|
|
138
|
+
readonly method: ManagedRelationalMethod;
|
|
139
|
+
}): PreparedStatement {
|
|
140
|
+
const source = statement.sql.trim();
|
|
141
|
+
const rewritten = rewriteProjection(source);
|
|
142
|
+
return {
|
|
143
|
+
statement: {
|
|
144
|
+
sql: rewritten.sql,
|
|
145
|
+
params: statement.params.map(relationalParameter),
|
|
146
|
+
method: statement.method,
|
|
147
|
+
},
|
|
148
|
+
projection: { lane: LANE, sql: source, columns: rewritten.columns },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* `sqlite-proxy` wants a flat row for `get` and an array of rows otherwise.
|
|
154
|
+
*
|
|
155
|
+
* A `get` that matched nothing must yield `undefined`, not an empty array:
|
|
156
|
+
* Drizzle's `mapGetResult` short-circuits on a FALSY row, and `[]` is truthy,
|
|
157
|
+
* so an empty array is mapped into an object whose every field is `undefined` —
|
|
158
|
+
* a "row" for a query that found none, which `if (exact) return true` in the
|
|
159
|
+
* personal block gate reads as a hit.
|
|
160
|
+
*/
|
|
114
161
|
function drizzleResult(
|
|
115
162
|
result: Awaited<
|
|
116
163
|
ReturnType<typeof parseManagedRelationalBatchResponse>
|
|
117
164
|
>["results"][number],
|
|
118
|
-
|
|
165
|
+
prepared: PreparedStatement,
|
|
119
166
|
) {
|
|
167
|
+
// sqlite-proxy exposes mutable `any[]` at this boundary even though the
|
|
168
|
+
// public runtime contract is intentionally immutable. `positionalRow` copies,
|
|
169
|
+
// so the provider-neutral contract never leaks a mutable result reference.
|
|
170
|
+
const rows = result.rows.map((row) =>
|
|
171
|
+
positionalRow(prepared.projection, result.columns, row),
|
|
172
|
+
);
|
|
120
173
|
return {
|
|
121
|
-
|
|
122
|
-
// public runtime contract is intentionally immutable. Copy here so the
|
|
123
|
-
// provider-neutral contract never leaks a mutable result reference.
|
|
124
|
-
rows:
|
|
125
|
-
method === "get"
|
|
126
|
-
? [...(result.rows[0] ?? [])]
|
|
127
|
-
: result.rows.map((row) => [...row]),
|
|
174
|
+
rows: (prepared.statement.method === "get" ? rows[0] : rows) as unknown[],
|
|
128
175
|
meta: result.meta,
|
|
129
176
|
};
|
|
130
177
|
}
|