orez-sync-cf-host 0.5.17 → 0.5.19

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/src/index.ts CHANGED
@@ -1,5 +1,17 @@
1
1
  export { createSyncDurableObject, createSyncWorker } from './host.js'
2
- export { MutationApplicationError, registerMutators } from './types.js'
2
+ export { createQueryCompiler } from './query-compiler.js'
3
+ export {
4
+ MutationApplicationError,
5
+ isMutationApplicationError,
6
+ registerMutators,
7
+ } from './types.js'
8
+ export {
9
+ DEFAULT_TRANSACTION_QUERY_BUDGET,
10
+ TransactionQueryBudgetError,
11
+ executeTransactionQueryPlan,
12
+ executeTransactionQueryPlanAsync,
13
+ } from './transaction-query.js'
14
+
3
15
  export type {
4
16
  DeferredEffect,
5
17
  JsonPrimitive,
@@ -20,3 +32,16 @@ export type {
20
32
  VisibilityFilter,
21
33
  ZeroSchemaConfig,
22
34
  } from './types.js'
35
+ export type {
36
+ CompiledTransactionQueryNode,
37
+ CompiledTransactionQueryPlan,
38
+ CompiledTransactionQueryRelationship,
39
+ TransactionQueryBinding,
40
+ TransactionQueryBudget,
41
+ TransactionQueryColumn,
42
+ TransactionQueryColumnType,
43
+ TransactionQueryExecutionOptions,
44
+ TransactionQueryFormat,
45
+ TransactionQueryWireValue,
46
+ } from './transaction-query.js'
47
+ export type { TransactionQueryCompiler } from './query-compiler.js'
@@ -24,6 +24,7 @@ type Fetcher = { fetch(input: string | Request, init?: RequestInit): Promise<Res
24
24
  interface Env extends SyncHostEnv {
25
25
  DATA: Fetcher
26
26
  APP: Fetcher
27
+ CAPPED_SYNC_DO: DurableObjectNamespace
27
28
  UPSTREAM_DO: DurableObjectNamespace
28
29
  }
29
30
 
@@ -32,15 +33,24 @@ const numericTextNamespaces = new Set<string>()
32
33
  const jsonValueNamespaces = new Set<string>()
33
34
  const hydratedNamespaces = new Set<string>()
34
35
  const heldSnapshots = new Set<string>()
36
+ const holdSnapshotsAfterCursor = new Set<string>()
35
37
  const activeSnapshots = new Set<string>()
38
+ const snapshotLimits = new Map<string, number[]>()
39
+ const heldDelegatedPushes = new Set<string>()
40
+ const activeDelegatedPushes = new Set<string>()
41
+ const completedDelegatedPushes = new Set<string>()
42
+ const heldChangeResponses = new Set<string>()
43
+ const activeHeldChangeResponses = new Set<string>()
36
44
  let delegatedFailuresRemaining = 0
37
45
  let delegatedAttempts = 0
38
46
  let delegatedPushFailedRemaining = 0
47
+ let delegatedUrl = ''
39
48
 
40
49
  const config: SyncHostConfig<Env> = {
41
50
  hostVersion: 'upstream-ingest-harness',
42
51
  schema,
43
- mutateUrl: '/api/zero/push',
52
+ mutateUrl: '/api/zero/push?schema=feed_0&appID=feed',
53
+ mutateOrigin: 'https://app.internal',
44
54
  mutateBinding: 'APP',
45
55
  delegatedPushRetry: {
46
56
  maxAttempts: 3,
@@ -50,9 +60,11 @@ const config: SyncHostConfig<Env> = {
50
60
  },
51
61
  upstream: {
52
62
  binding: 'DATA',
53
- namespacePath: (namespace) => `/${namespace}`,
63
+ namespacePath: (namespace) =>
64
+ namespace.startsWith('root-mount-') ? '/' : `/${namespace}`,
54
65
  changeLimit: 2,
55
66
  intervalMs: 1_000,
67
+ ingestBudgetRows: 600,
56
68
  },
57
69
  initialize(sql) {
58
70
  sql.exec(
@@ -63,12 +75,23 @@ const config: SyncHostConfig<Env> = {
63
75
  const userID = request.headers.get('authorization')?.match(/^Bearer token-(.+)$/)?.[1]
64
76
  return userID ? { userID } : null
65
77
  },
78
+ authorizeWake(request) {
79
+ return new URL(request.url).searchParams.get('wakeToken') === 'ingest-harness-wake'
80
+ },
81
+ authorizeNotify(request, env) {
82
+ return Boolean(env.ADMIN_KEY) && request.headers.get('x-admin-key') === env.ADMIN_KEY
83
+ },
66
84
  namespace(request) {
67
85
  return new URL(request.url).pathname.split('/')[1] || null
68
86
  },
69
87
  }
70
88
 
71
89
  export const SyncDurableObject = createSyncDurableObject(config)
90
+ const cappedConfig: SyncHostConfig<Env> = {
91
+ ...config,
92
+ caps: { maxChangeRows: 10_000, maxChangeBytes: 1 },
93
+ }
94
+ export const CappedSyncDurableObject = createSyncDurableObject(cappedConfig)
72
95
  export { ZeroDO }
73
96
 
74
97
  async function upstreamFetch(request: Request, env: Env): Promise<Response> {
@@ -113,6 +136,35 @@ export class DataService extends WorkerEntrypoint<Env> {
113
136
  new Response('DATA route rejected non-feed request', { status: 418 })
114
137
  )
115
138
  }
139
+ if (pathname === '/changes') {
140
+ const cursor = Number(url.searchParams.get('watermark') ?? 0)
141
+ return Promise.resolve(
142
+ Response.json({
143
+ watermark: 1,
144
+ changes:
145
+ cursor >= 1
146
+ ? []
147
+ : [
148
+ {
149
+ watermark: 1,
150
+ tableName: 'item',
151
+ op: 'INSERT',
152
+ rowData: {
153
+ id: 'root-feed-row',
154
+ label: 'root-mounted upstream feed',
155
+ rank: 1,
156
+ done: false,
157
+ meta: null,
158
+ },
159
+ oldData: null,
160
+ },
161
+ ],
162
+ })
163
+ )
164
+ }
165
+ if (pathname === '/_orez/write-budget') {
166
+ return Promise.resolve(Response.json({ enabled: true, rootMount: true }))
167
+ }
116
168
  const namespace = pathname.split('/')[1] ?? ''
117
169
  if (pathname.endsWith('/changes') && jsonValueNamespaces.has(namespace)) {
118
170
  const cursor = Number(new URL(request.url).searchParams.get('watermark') ?? 0)
@@ -210,11 +262,33 @@ export class DataService extends WorkerEntrypoint<Env> {
210
262
  )
211
263
  }
212
264
  const response = await upstreamFetch(request, this.env)
265
+ if (pathname.endsWith('/changes') && heldChangeResponses.has(namespace)) {
266
+ activeHeldChangeResponses.add(namespace)
267
+ try {
268
+ while (heldChangeResponses.has(namespace)) await scheduler.wait(10)
269
+ } finally {
270
+ activeHeldChangeResponses.delete(namespace)
271
+ }
272
+ }
213
273
  if (pathname.endsWith('/snapshot')) {
214
- if (heldSnapshots.has(namespace)) {
274
+ const limit = Number(url.searchParams.get('limit'))
275
+ if (Number.isSafeInteger(limit)) {
276
+ const limits = snapshotLimits.get(namespace) ?? []
277
+ limits.push(limit)
278
+ snapshotLimits.set(namespace, limits)
279
+ }
280
+ const shouldHold =
281
+ heldSnapshots.has(namespace) &&
282
+ (!holdSnapshotsAfterCursor.has(namespace) || url.searchParams.has('cursor'))
283
+ if (shouldHold) {
215
284
  activeSnapshots.add(namespace)
216
285
  try {
217
- while (heldSnapshots.has(namespace)) await scheduler.wait(10)
286
+ while (
287
+ heldSnapshots.has(namespace) &&
288
+ (!holdSnapshotsAfterCursor.has(namespace) || url.searchParams.has('cursor'))
289
+ ) {
290
+ await scheduler.wait(10)
291
+ }
218
292
  } finally {
219
293
  activeSnapshots.delete(namespace)
220
294
  }
@@ -226,7 +300,8 @@ export class DataService extends WorkerEntrypoint<Env> {
226
300
  }
227
301
 
228
302
  export class AppService extends WorkerEntrypoint<Env> {
229
- fetch(request: Request): Promise<Response> {
303
+ async fetch(request: Request): Promise<Response> {
304
+ delegatedUrl = request.url
230
305
  if (!new URL(request.url).pathname.endsWith('/api/zero/push')) {
231
306
  return Promise.resolve(
232
307
  new Response('APP route rejected non-push request', { status: 418 })
@@ -257,14 +332,81 @@ export class AppService extends WorkerEntrypoint<Env> {
257
332
  Response.json({ error: 'synthetic delegated push failure' }, { status: 503 })
258
333
  )
259
334
  }
260
- return upstreamFetch(request, this.env)
335
+ if (heldDelegatedPushes.has(namespace)) {
336
+ activeDelegatedPushes.add(namespace)
337
+ try {
338
+ while (heldDelegatedPushes.has(namespace)) await scheduler.wait(10)
339
+ } finally {
340
+ activeDelegatedPushes.delete(namespace)
341
+ }
342
+ }
343
+ const push = (await request.clone().json()) as {
344
+ mutations?: Array<{ clientID?: string; id?: number; name?: string }>
345
+ }
346
+ const cleanupIDs = new Set(
347
+ (push.mutations ?? [])
348
+ .filter((mutation) => mutation.name === '_zero_cleanupResults')
349
+ .map((mutation) => `${mutation.clientID}:${mutation.id}`)
350
+ )
351
+ const response = await upstreamFetch(request, this.env)
352
+ completedDelegatedPushes.add(namespace)
353
+ if (response.ok && cleanupIDs.size > 0) {
354
+ const body = (await response.json()) as {
355
+ mutations?: Array<{ id?: { clientID?: string; id?: number } }>
356
+ pushResponse?: {
357
+ mutations?: Array<{ id?: { clientID?: string; id?: number } }>
358
+ }
359
+ }
360
+ const mutations = body.pushResponse?.mutations ?? body.mutations
361
+ if (Array.isArray(mutations)) {
362
+ const filtered = mutations.filter(
363
+ (mutation) => !cleanupIDs.has(`${mutation.id?.clientID}:${mutation.id?.id}`)
364
+ )
365
+ if (body.pushResponse) body.pushResponse.mutations = filtered
366
+ else body.mutations = filtered
367
+ }
368
+ return Response.json(body, { status: response.status })
369
+ }
370
+ return response
261
371
  }
262
372
  }
263
373
 
264
374
  const syncWorker = createSyncWorker(config)
375
+ const cappedSyncWorker = createSyncWorker(cappedConfig)
265
376
  export default {
266
377
  fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
267
378
  const url = new URL(request.url)
379
+ if (url.pathname.startsWith('/delegated-ingest-control/')) {
380
+ const namespace = url.pathname.slice('/delegated-ingest-control/'.length)
381
+ if (request.method === 'GET') {
382
+ return Promise.resolve(
383
+ Response.json({
384
+ appHeld: heldDelegatedPushes.has(namespace),
385
+ appActive: activeDelegatedPushes.has(namespace),
386
+ appCompleted: completedDelegatedPushes.has(namespace),
387
+ changesHeld: heldChangeResponses.has(namespace),
388
+ changesActive: activeHeldChangeResponses.has(namespace),
389
+ })
390
+ )
391
+ }
392
+ return request
393
+ .json()
394
+ .catch(() => ({}))
395
+ .then((body) => {
396
+ if ((body as { appHeld?: unknown }).appHeld === true) {
397
+ heldDelegatedPushes.add(namespace)
398
+ completedDelegatedPushes.delete(namespace)
399
+ } else if ((body as { appHeld?: unknown }).appHeld === false) {
400
+ heldDelegatedPushes.delete(namespace)
401
+ }
402
+ if ((body as { changesHeld?: unknown }).changesHeld === true) {
403
+ heldChangeResponses.add(namespace)
404
+ } else if ((body as { changesHeld?: unknown }).changesHeld === false) {
405
+ heldChangeResponses.delete(namespace)
406
+ }
407
+ return Response.json({ ok: true, namespace })
408
+ })
409
+ }
268
410
  if (url.pathname.startsWith('/snapshot-control/')) {
269
411
  const namespace = url.pathname.slice('/snapshot-control/'.length)
270
412
  if (request.method === 'GET') {
@@ -272,6 +414,8 @@ export default {
272
414
  Response.json({
273
415
  active: activeSnapshots.has(namespace),
274
416
  held: heldSnapshots.has(namespace),
417
+ afterCursor: holdSnapshotsAfterCursor.has(namespace),
418
+ limits: snapshotLimits.get(namespace) ?? [],
275
419
  })
276
420
  )
277
421
  }
@@ -281,12 +425,22 @@ export default {
281
425
  .then((body) => {
282
426
  if ((body as { hold?: unknown }).hold === true) {
283
427
  heldSnapshots.add(namespace)
428
+ if ((body as { afterCursor?: unknown }).afterCursor === true) {
429
+ holdSnapshotsAfterCursor.add(namespace)
430
+ } else {
431
+ holdSnapshotsAfterCursor.delete(namespace)
432
+ }
284
433
  } else {
285
434
  heldSnapshots.delete(namespace)
435
+ holdSnapshotsAfterCursor.delete(namespace)
286
436
  }
437
+ if ((body as { reset?: unknown }).reset === true)
438
+ snapshotLimits.set(namespace, [])
287
439
  return Response.json({
288
440
  active: activeSnapshots.has(namespace),
289
441
  held: heldSnapshots.has(namespace),
442
+ afterCursor: holdSnapshotsAfterCursor.has(namespace),
443
+ limits: snapshotLimits.get(namespace) ?? [],
290
444
  })
291
445
  })
292
446
  }
@@ -337,6 +491,7 @@ export default {
337
491
  delegatedFailuresRemaining,
338
492
  delegatedPushFailedRemaining,
339
493
  delegatedAttempts,
494
+ delegatedUrl,
340
495
  })
341
496
  )
342
497
  }
@@ -353,13 +508,24 @@ export default {
353
508
  Number((body as { pushFailed?: unknown }).pushFailed) || 0
354
509
  )
355
510
  delegatedAttempts = 0
511
+ delegatedUrl = ''
356
512
  return Response.json({
357
513
  delegatedFailuresRemaining,
358
514
  delegatedPushFailedRemaining,
359
515
  delegatedAttempts,
516
+ delegatedUrl,
360
517
  })
361
518
  })
362
519
  }
520
+ if (url.pathname.startsWith('/capped/')) {
521
+ url.pathname = url.pathname.slice('/capped'.length)
522
+ const cappedEnv = { ...env, SYNC_DO: env.CAPPED_SYNC_DO }
523
+ return cappedSyncWorker.fetch!(
524
+ new Request(url, request) as never,
525
+ cappedEnv,
526
+ ctx
527
+ ) as Promise<Response>
528
+ }
363
529
  if (url.pathname.startsWith('/upstream/')) {
364
530
  url.pathname = url.pathname.slice('/upstream'.length)
365
531
  return upstreamFetch(new Request(url, request), env)
@@ -0,0 +1,28 @@
1
+ // the app-rejection contract shared by every orez sync host and by
2
+ // protocol-neutral authoring layers (on-zero adapters). classification is
3
+ // STRUCTURAL, never instanceof: adapters must not depend on orez to reject a
4
+ // mutation, and instanceof breaks across package copies (dual-package
5
+ // installs, --into tarballs, workspace duplication). any error whose name is
6
+ // 'MutationApplicationError' with string details/message is claiming this
7
+ // contract deliberately.
8
+ export class MutationApplicationError extends Error {
9
+ constructor(
10
+ readonly details: string,
11
+ message = details
12
+ ) {
13
+ super(message)
14
+ this.name = 'MutationApplicationError'
15
+ }
16
+ }
17
+
18
+ export function isMutationApplicationError(
19
+ error: unknown
20
+ ): error is { name: string; message: string; details: string } {
21
+ if (typeof error !== 'object' || error === null) return false
22
+ const shaped = error as { name?: unknown; message?: unknown; details?: unknown }
23
+ return (
24
+ shaped.name === 'MutationApplicationError' &&
25
+ typeof shaped.message === 'string' &&
26
+ typeof shaped.details === 'string'
27
+ )
28
+ }
@@ -1,18 +1,21 @@
1
- import { DurableObject } from 'cloudflare:workers'
2
-
1
+ import { ZeroDO } from '../../../src/cf-do/worker.js'
2
+ import { createQueryCompiler } from './query-compiler.js'
3
+ import {
4
+ SqlStorageDirect,
5
+ SqlStorageMutatorTransaction,
6
+ SqlStorageSyncDb,
7
+ } from './sql-storage-adapter.js'
3
8
  import {
4
- initSync,
5
9
  init_probe_schema,
6
10
  pull_snapshot,
7
11
  push_finalize,
8
12
  push_preflight,
9
13
  rust_panic_after_writes,
10
14
  value_round_trip,
11
- } from './generated/sync_wasm.js'
12
- import wasmModule from './generated/sync_wasm_bg.wasm'
13
- import { SqlStorageSyncDb } from './sql-storage-adapter.js'
15
+ } from './wasm-platform.js'
14
16
 
15
- initSync({ module: wasmModule })
17
+ import type { TransactionQueryFormat } from './transaction-query.js'
18
+ import type { ZeroSchemaConfig } from './types.js'
16
19
 
17
20
  interface Env {
18
21
  PROBE_DO: DurableObjectNamespace<ProbeDurableObject>
@@ -21,6 +24,54 @@ interface Env {
21
24
  type DeferredEffect = { mutationID: string; kind: string }
22
25
  type MutatorName = 'read-then-write' | 'multi-table' | 'application-error'
23
26
 
27
+ const transactionQuerySchema = {
28
+ tables: {
29
+ account: {
30
+ name: 'account',
31
+ serverName: 'accounts',
32
+ columns: {
33
+ id: { type: 'string' },
34
+ balance: { type: 'number' },
35
+ },
36
+ primaryKey: ['id'],
37
+ },
38
+ entry: {
39
+ name: 'entry',
40
+ serverName: 'ledger',
41
+ columns: {
42
+ id: { type: 'number' },
43
+ accountId: { type: 'string', serverName: 'account_id' },
44
+ amount: { type: 'number' },
45
+ note: { type: 'string' },
46
+ },
47
+ primaryKey: ['id'],
48
+ },
49
+ },
50
+ } as const satisfies ZeroSchemaConfig
51
+
52
+ const transactionQueryAst = {
53
+ table: 'account',
54
+ where: {
55
+ type: 'simple',
56
+ op: '=',
57
+ left: { type: 'column', name: 'id' },
58
+ right: { type: 'literal', value: 'primary' },
59
+ },
60
+ related: [
61
+ {
62
+ correlation: { parentField: ['id'], childField: ['accountId'] },
63
+ subquery: { table: 'entry', alias: 'entries', orderBy: [['id', 'asc']] },
64
+ },
65
+ ],
66
+ }
67
+
68
+ const transactionQueryFormat = {
69
+ singular: true,
70
+ relationships: { entries: { singular: false, relationships: {} } },
71
+ } as const satisfies TransactionQueryFormat
72
+
73
+ const compileTransactionQuery = createQueryCompiler(transactionQuerySchema)
74
+
24
75
  // Deterministic local analogue of normal DO idle eviction, matching the
25
76
  // harness/cf probe: discard all in-memory state after an idle gap while SQL
26
77
  // storage remains intact. This keeps the test sub-second instead of waiting
@@ -33,7 +84,7 @@ const json = (value: unknown, status = 200) =>
33
84
  headers: { 'cache-control': 'no-store' },
34
85
  })
35
86
 
36
- export class ProbeDurableObject extends DurableObject<Env> {
87
+ export class ProbeDurableObject extends ZeroDO {
37
88
  readonly #db: SqlStorageSyncDb
38
89
  #bootID = crypto.randomUUID()
39
90
  #lastRequestAt = 0
@@ -41,7 +92,7 @@ export class ProbeDurableObject extends DurableObject<Env> {
41
92
  #effects: Array<DeferredEffect & { observedCommitted: boolean }> = []
42
93
 
43
94
  constructor(ctx: DurableObjectState, env: Env) {
44
- super(ctx, env)
95
+ super(ctx, env as never)
45
96
  this.#db = new SqlStorageSyncDb(ctx.storage.sql)
46
97
  ctx.storage.transactionSync(() => init_probe_schema(this.#db))
47
98
  }
@@ -291,6 +342,68 @@ export class ProbeDurableObject extends DurableObject<Env> {
291
342
  return json({ errors })
292
343
  }
293
344
 
345
+ if (route === '/transaction-query') {
346
+ let plan: ReturnType<typeof compileTransactionQuery> | undefined
347
+ const tx = new SqlStorageMutatorTransaction(
348
+ new SqlStorageDirect(this.ctx.storage.sql),
349
+ (queryAst, queryFormat) => {
350
+ plan = compileTransactionQuery(queryAst, queryFormat)
351
+ return plan
352
+ }
353
+ )
354
+ const result = await tx.queryAst(
355
+ transactionQueryAst,
356
+ transactionQueryFormat,
357
+ 'platformTransactionQuery'
358
+ )
359
+ let malformedFormatStatus: unknown
360
+ try {
361
+ await tx.queryAst(transactionQueryAst, undefined as never)
362
+ } catch (error) {
363
+ malformedFormatStatus = (error as { status?: unknown }).status
364
+ }
365
+ return json({ result, malformedFormatStatus, plan })
366
+ }
367
+
368
+ if (route === '/application-transaction-query') {
369
+ const result = await this.runApplicationTransaction(
370
+ compileTransactionQuery,
371
+ (tx) =>
372
+ tx.queryAst(
373
+ transactionQueryAst,
374
+ transactionQueryFormat,
375
+ 'applicationTransactionQuery'
376
+ ),
377
+ { maxSelects: 8, maxRows: 20 }
378
+ )
379
+ return json({ result })
380
+ }
381
+
382
+ if (route === '/application-transaction-query-budget') {
383
+ try {
384
+ await this.runApplicationTransaction(
385
+ compileTransactionQuery,
386
+ (tx) =>
387
+ tx.queryAst(
388
+ transactionQueryAst,
389
+ transactionQueryFormat,
390
+ 'budgetedApplicationTransactionQuery'
391
+ ),
392
+ { maxSelects: 1 }
393
+ )
394
+ } catch (error) {
395
+ return json(
396
+ {
397
+ code: Reflect.get(error as object, 'code'),
398
+ query: Reflect.get(error as object, 'query'),
399
+ selects: Reflect.get(error as object, 'selects'),
400
+ },
401
+ 409
402
+ )
403
+ }
404
+ return json({ error: 'query budget did not abort the transaction' }, 500)
405
+ }
406
+
294
407
  return json({ error: 'not found', route }, 404)
295
408
  }
296
409
  }
@@ -0,0 +1,40 @@
1
+ export type DeferredEffect = () => void | Promise<void>
2
+
3
+ export type PostCommitEffects = {
4
+ /** discard effects from a storage-transaction attempt before starting another */
5
+ beginAttempt(): void
6
+ defer(effect: DeferredEffect): void
7
+ /** run the committed attempt's effects in order without rejecting */
8
+ runAfterCommit(onError: (error: unknown) => void): Promise<void>
9
+ }
10
+
11
+ /**
12
+ * collect external work during a retryable transaction and run only the effects
13
+ * belonging to the attempt that commits.
14
+ */
15
+ export function createPostCommitEffects(): PostCommitEffects {
16
+ const effects: DeferredEffect[] = []
17
+
18
+ return {
19
+ beginAttempt() {
20
+ effects.length = 0
21
+ },
22
+ defer(effect) {
23
+ effects.push(effect)
24
+ },
25
+ async runAfterCommit(onError) {
26
+ const committed = effects.splice(0)
27
+ for (const effect of committed) {
28
+ try {
29
+ await effect()
30
+ } catch (error) {
31
+ try {
32
+ onError(error)
33
+ } catch {
34
+ // reporting must not turn a committed effect failure into a rejection
35
+ }
36
+ }
37
+ }
38
+ },
39
+ }
40
+ }
@@ -0,0 +1,16 @@
1
+ import { engine_compile_query } from './wasm.js'
2
+
3
+ import type {
4
+ CompiledTransactionQueryPlan,
5
+ TransactionQueryFormat,
6
+ } from './transaction-query.js'
7
+ import type { ZeroSchemaConfig } from './types.js'
8
+
9
+ export type TransactionQueryCompiler = (
10
+ ast: unknown,
11
+ format: TransactionQueryFormat
12
+ ) => CompiledTransactionQueryPlan
13
+
14
+ export function createQueryCompiler(schema: ZeroSchemaConfig): TransactionQueryCompiler {
15
+ return (ast, format) => engine_compile_query(schema, ast, format)
16
+ }