@lpdjs/firestore-repo-service 2.2.3 → 2.2.5
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.md +40 -19
- package/dist/create-servers-D4-NGpKm.d.ts +105 -0
- package/dist/create-servers-DmggzSb3.d.cts +105 -0
- package/dist/{index-Bw1fvzu5.d.ts → index-Cvip2Sgt.d.ts} +2 -2
- package/dist/{index-bqqrGjxe.d.cts → index-DpD4DEqH.d.cts} +2 -2
- package/dist/index.cjs +137 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +137 -47
- package/dist/index.js.map +1 -1
- package/dist/openapi-B3P2F8op.d.ts +69 -0
- package/dist/openapi-UJJ1aCFk.d.cts +69 -0
- package/dist/queue-D_-aMf4H.d.ts +52 -0
- package/dist/queue-Dk1Ezhkf.d.cts +52 -0
- package/dist/servers/admin/index.d.cts +2 -2
- package/dist/servers/admin/index.d.ts +2 -2
- package/dist/servers/crud/index.cjs.map +1 -1
- package/dist/servers/crud/index.d.cts +4 -2
- package/dist/servers/crud/index.d.ts +4 -2
- package/dist/servers/crud/index.js.map +1 -1
- package/dist/servers/index.cjs +157 -67
- package/dist/servers/index.cjs.map +1 -1
- package/dist/servers/index.d.cts +9 -4
- package/dist/servers/index.d.ts +9 -4
- package/dist/servers/index.js +157 -67
- package/dist/servers/index.js.map +1 -1
- package/dist/sync/bigquery.d.cts +1 -1
- package/dist/sync/bigquery.d.ts +1 -1
- package/dist/sync/index.cjs +33 -33
- package/dist/sync/index.cjs.map +1 -1
- package/dist/sync/index.d.cts +5 -107
- package/dist/sync/index.d.ts +5 -107
- package/dist/sync/index.js +33 -33
- package/dist/sync/index.js.map +1 -1
- package/dist/{types-BbCdscqh.d.cts → types-CX5AbZWV.d.cts} +1 -1
- package/dist/{types-BbCdscqh.d.ts → types-CX5AbZWV.d.ts} +1 -1
- package/dist/{types-DKgWRTDf.d.cts → types-Z9Qy8Xmx.d.cts} +7 -68
- package/dist/{types-DKgWRTDf.d.ts → types-Z9Qy8Xmx.d.ts} +7 -68
- package/package.json +1 -1
package/dist/sync/index.d.cts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { A as AdminHttpsOptions, h as FirestoreTriggersDep, O as OrFactory, i as PubSubHandlerDep, j as SyncDeps, k as SyncOperation, l as SyncWorkerOptions, m as adminsyncBasicAuth, n as adminsyncFeaturesFlag } from '../types-
|
|
1
|
+
import { a as SqlAdapter, S as SyncEvent, b as adminsyncConfig, R as RepoSyncConfig, P as PubSubClientDep, c as SqlDialect, d as SqlColumn, e as SqlTableDef, G as GenerateDDLConfig, L as LogicalType, f as SyncTriggersConfig, g as SyncWorkerConfig } from '../types-CX5AbZWV.cjs';
|
|
2
|
+
export { A as AdminHttpsOptions, F as FirestoreSyncConfig, h as FirestoreTriggersDep, O as OrFactory, i as PubSubHandlerDep, j as SyncDeps, k as SyncOperation, l as SyncWorkerOptions, m as adminsyncBasicAuth, n as adminsyncFeaturesFlag } from '../types-CX5AbZWV.cjs';
|
|
3
|
+
import { S as SyncQueue } from '../queue-Dk1Ezhkf.cjs';
|
|
4
|
+
export { a as SyncQueueOptions } from '../queue-Dk1Ezhkf.cjs';
|
|
3
5
|
import { z } from 'zod';
|
|
4
6
|
import * as firebase_functions_core from 'firebase-functions/core';
|
|
5
7
|
import * as firebase_functions_pubsub from 'firebase-functions/pubsub';
|
|
@@ -7,55 +9,6 @@ import 'firebase-functions/v2/firestore';
|
|
|
7
9
|
import 'firebase-functions/v2/https';
|
|
8
10
|
import 'firebase-functions/v2/pubsub';
|
|
9
11
|
|
|
10
|
-
/**
|
|
11
|
-
* Per-repo in-memory batch buffer.
|
|
12
|
-
*
|
|
13
|
-
* Accumulates {@link SyncEvent}s and flushes them in batches to a
|
|
14
|
-
* {@link SqlAdapter}. On flush failure the events are re-published
|
|
15
|
-
* to PubSub for retry (if a PubSub re-publisher is provided).
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
interface SyncQueueOptions {
|
|
19
|
-
/** SQL adapter to flush data to */
|
|
20
|
-
adapter: SqlAdapter;
|
|
21
|
-
/** SQL table name */
|
|
22
|
-
tableName: string;
|
|
23
|
-
/** Primary key column name */
|
|
24
|
-
primaryKey: string;
|
|
25
|
-
/** Max rows per flush (default: 100) */
|
|
26
|
-
batchSize?: number;
|
|
27
|
-
/** Auto-flush interval in ms (default: 5_000). 0 = manual only. */
|
|
28
|
-
flushIntervalMs?: number;
|
|
29
|
-
/** Called on flush failure with the failed events. Typically re-publishes to PubSub. */
|
|
30
|
-
onFlushError?: (events: SyncEvent[], error: unknown) => Promise<void>;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* In-memory buffer that batches sync events per-repo and flushes them
|
|
34
|
-
* to a SQL adapter.
|
|
35
|
-
*/
|
|
36
|
-
declare class SyncQueue {
|
|
37
|
-
private buffer;
|
|
38
|
-
private flushing;
|
|
39
|
-
private timer;
|
|
40
|
-
private readonly adapter;
|
|
41
|
-
private readonly tableName;
|
|
42
|
-
private readonly primaryKey;
|
|
43
|
-
private readonly batchSize;
|
|
44
|
-
private readonly onFlushError?;
|
|
45
|
-
constructor(opts: SyncQueueOptions);
|
|
46
|
-
/** Number of events waiting in the buffer. */
|
|
47
|
-
get size(): number;
|
|
48
|
-
/** Push one or more events into the buffer. Triggers auto-flush if batchSize reached. */
|
|
49
|
-
enqueue(...events: SyncEvent[]): void;
|
|
50
|
-
/**
|
|
51
|
-
* Flush all buffered events to the SQL adapter.
|
|
52
|
-
* Upserts and deletes are batched separately.
|
|
53
|
-
*/
|
|
54
|
-
flush(): Promise<void>;
|
|
55
|
-
/** Stop the auto-flush timer and flush remaining events. */
|
|
56
|
-
shutdown(): Promise<void>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
12
|
/**
|
|
60
13
|
* Sync Admin — optional HTTP endpoint for inspecting and managing the
|
|
61
14
|
* Firestore → SQL sync pipeline.
|
|
@@ -93,61 +46,6 @@ declare class SyncQueue {
|
|
|
93
46
|
*/
|
|
94
47
|
declare function createadminsyncServer(repoMapping: Record<string, any>, adapter: SqlAdapter, queues: Map<string, SyncQueue>, handleMessage: (event: SyncEvent) => Promise<void>, config: adminsyncConfig, repoConfigs: Record<string, RepoSyncConfig<string> | undefined>, pubsub?: PubSubClientDep, topicPrefix?: string): (req: any, res: any) => Promise<void>;
|
|
95
48
|
|
|
96
|
-
/**
|
|
97
|
-
* Unified wrapper — combines triggers + worker into a single call.
|
|
98
|
-
*
|
|
99
|
-
* @example
|
|
100
|
-
* ```typescript
|
|
101
|
-
* import * as firestoreTriggers from "firebase-functions/v2/firestore";
|
|
102
|
-
* import * as pubsubHandler from "firebase-functions/v2/pubsub";
|
|
103
|
-
* import { PubSub } from "@google-cloud/pubsub";
|
|
104
|
-
*
|
|
105
|
-
* const sync = createFirestoreSync(repos, {
|
|
106
|
-
* deps: { firestoreTriggers, pubsubHandler, pubsub: new PubSub() },
|
|
107
|
-
* adapter,
|
|
108
|
-
* topicPrefix: "firestore-sync",
|
|
109
|
-
* autoMigrate: true,
|
|
110
|
-
* admin: {
|
|
111
|
-
* auth: { type: "basic", username: "admin", password: "secret" },
|
|
112
|
-
* featuresFlag: { healthCheck: true, manualSync: true, viewQueue: true },
|
|
113
|
-
* },
|
|
114
|
-
* repos: {
|
|
115
|
-
* users: { exclude: ["documentPath"], columnMap: { docId: "user_id" } },
|
|
116
|
-
* posts: { columnMap: { docId: "post_id" } },
|
|
117
|
-
* },
|
|
118
|
-
* });
|
|
119
|
-
*
|
|
120
|
-
* // Triggers + PubSub handlers
|
|
121
|
-
* export const { users_onCreate, users_onUpdate, users_onDelete, sync_users } = sync.functions;
|
|
122
|
-
*
|
|
123
|
-
* // Admin endpoint — wrap with onRequest yourself
|
|
124
|
-
* export const adminsync = onRequest(sync.adminHandler!);
|
|
125
|
-
*
|
|
126
|
-
* // Or pass onRequest in admin config to auto-add to sync.functions:
|
|
127
|
-
* // admin: { onRequest, ... } → export const { adminsync } = sync.functions;
|
|
128
|
-
* ```
|
|
129
|
-
*/
|
|
130
|
-
|
|
131
|
-
declare function createFirestoreSync<M extends Record<string, any>>(repoMapping: M, config: FirestoreSyncConfig<NoInfer<M>>): {
|
|
132
|
-
/** All Cloud Functions (triggers + handlers + optional admin) — spread into exports */
|
|
133
|
-
functions: Record<string, any>;
|
|
134
|
-
/**
|
|
135
|
-
* Raw admin HTTP handler — wrap with `onRequest()` yourself if you
|
|
136
|
-
* didn't pass `onRequest` in the admin config.
|
|
137
|
-
* @example
|
|
138
|
-
* ```ts
|
|
139
|
-
* export const adminsync = onRequest(sync.adminHandler!);
|
|
140
|
-
* ```
|
|
141
|
-
*/
|
|
142
|
-
adminHandler: ((req: any, res: any) => Promise<void>) | null;
|
|
143
|
-
/** Process a SyncEvent directly (for testing) */
|
|
144
|
-
handleMessage: (event: SyncEvent) => Promise<void>;
|
|
145
|
-
/** Internal queue map (for testing) */
|
|
146
|
-
queues: Map<string, SyncQueue>;
|
|
147
|
-
/** Flush all queues and stop timers */
|
|
148
|
-
shutdown: () => Promise<void>;
|
|
149
|
-
};
|
|
150
|
-
|
|
151
49
|
/**
|
|
152
50
|
* DDL generator — produces CREATE TABLE / ALTER TABLE statements from
|
|
153
51
|
* SqlColumn definitions and a SqlDialect.
|
|
@@ -308,4 +206,4 @@ declare function createSyncWorker<M extends Record<string, any>>(repoMapping: M,
|
|
|
308
206
|
shutdown(): Promise<void>;
|
|
309
207
|
};
|
|
310
208
|
|
|
311
|
-
export {
|
|
209
|
+
export { GenerateDDLConfig, LogicalType, type MigrateResult, PubSubClientDep, RepoSyncConfig, SqlAdapter, SqlColumn, SqlDialect, SqlTableDef, SyncEvent, SyncQueue, SyncTriggersConfig, SyncWorkerConfig, addColumnsDDL, adminsyncConfig, autoMigrate, createSyncTriggers, createSyncWorker, createTableDDL, createadminsyncServer, generateDDL, serializeDocument, serializeValue, zodSchemaToColumns, zodTypeToLogical };
|
package/dist/sync/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { A as AdminHttpsOptions, h as FirestoreTriggersDep, O as OrFactory, i as PubSubHandlerDep, j as SyncDeps, k as SyncOperation, l as SyncWorkerOptions, m as adminsyncBasicAuth, n as adminsyncFeaturesFlag } from '../types-
|
|
1
|
+
import { a as SqlAdapter, S as SyncEvent, b as adminsyncConfig, R as RepoSyncConfig, P as PubSubClientDep, c as SqlDialect, d as SqlColumn, e as SqlTableDef, G as GenerateDDLConfig, L as LogicalType, f as SyncTriggersConfig, g as SyncWorkerConfig } from '../types-CX5AbZWV.js';
|
|
2
|
+
export { A as AdminHttpsOptions, F as FirestoreSyncConfig, h as FirestoreTriggersDep, O as OrFactory, i as PubSubHandlerDep, j as SyncDeps, k as SyncOperation, l as SyncWorkerOptions, m as adminsyncBasicAuth, n as adminsyncFeaturesFlag } from '../types-CX5AbZWV.js';
|
|
3
|
+
import { S as SyncQueue } from '../queue-D_-aMf4H.js';
|
|
4
|
+
export { a as SyncQueueOptions } from '../queue-D_-aMf4H.js';
|
|
3
5
|
import { z } from 'zod';
|
|
4
6
|
import * as firebase_functions_core from 'firebase-functions/core';
|
|
5
7
|
import * as firebase_functions_pubsub from 'firebase-functions/pubsub';
|
|
@@ -7,55 +9,6 @@ import 'firebase-functions/v2/firestore';
|
|
|
7
9
|
import 'firebase-functions/v2/https';
|
|
8
10
|
import 'firebase-functions/v2/pubsub';
|
|
9
11
|
|
|
10
|
-
/**
|
|
11
|
-
* Per-repo in-memory batch buffer.
|
|
12
|
-
*
|
|
13
|
-
* Accumulates {@link SyncEvent}s and flushes them in batches to a
|
|
14
|
-
* {@link SqlAdapter}. On flush failure the events are re-published
|
|
15
|
-
* to PubSub for retry (if a PubSub re-publisher is provided).
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
interface SyncQueueOptions {
|
|
19
|
-
/** SQL adapter to flush data to */
|
|
20
|
-
adapter: SqlAdapter;
|
|
21
|
-
/** SQL table name */
|
|
22
|
-
tableName: string;
|
|
23
|
-
/** Primary key column name */
|
|
24
|
-
primaryKey: string;
|
|
25
|
-
/** Max rows per flush (default: 100) */
|
|
26
|
-
batchSize?: number;
|
|
27
|
-
/** Auto-flush interval in ms (default: 5_000). 0 = manual only. */
|
|
28
|
-
flushIntervalMs?: number;
|
|
29
|
-
/** Called on flush failure with the failed events. Typically re-publishes to PubSub. */
|
|
30
|
-
onFlushError?: (events: SyncEvent[], error: unknown) => Promise<void>;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* In-memory buffer that batches sync events per-repo and flushes them
|
|
34
|
-
* to a SQL adapter.
|
|
35
|
-
*/
|
|
36
|
-
declare class SyncQueue {
|
|
37
|
-
private buffer;
|
|
38
|
-
private flushing;
|
|
39
|
-
private timer;
|
|
40
|
-
private readonly adapter;
|
|
41
|
-
private readonly tableName;
|
|
42
|
-
private readonly primaryKey;
|
|
43
|
-
private readonly batchSize;
|
|
44
|
-
private readonly onFlushError?;
|
|
45
|
-
constructor(opts: SyncQueueOptions);
|
|
46
|
-
/** Number of events waiting in the buffer. */
|
|
47
|
-
get size(): number;
|
|
48
|
-
/** Push one or more events into the buffer. Triggers auto-flush if batchSize reached. */
|
|
49
|
-
enqueue(...events: SyncEvent[]): void;
|
|
50
|
-
/**
|
|
51
|
-
* Flush all buffered events to the SQL adapter.
|
|
52
|
-
* Upserts and deletes are batched separately.
|
|
53
|
-
*/
|
|
54
|
-
flush(): Promise<void>;
|
|
55
|
-
/** Stop the auto-flush timer and flush remaining events. */
|
|
56
|
-
shutdown(): Promise<void>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
12
|
/**
|
|
60
13
|
* Sync Admin — optional HTTP endpoint for inspecting and managing the
|
|
61
14
|
* Firestore → SQL sync pipeline.
|
|
@@ -93,61 +46,6 @@ declare class SyncQueue {
|
|
|
93
46
|
*/
|
|
94
47
|
declare function createadminsyncServer(repoMapping: Record<string, any>, adapter: SqlAdapter, queues: Map<string, SyncQueue>, handleMessage: (event: SyncEvent) => Promise<void>, config: adminsyncConfig, repoConfigs: Record<string, RepoSyncConfig<string> | undefined>, pubsub?: PubSubClientDep, topicPrefix?: string): (req: any, res: any) => Promise<void>;
|
|
95
48
|
|
|
96
|
-
/**
|
|
97
|
-
* Unified wrapper — combines triggers + worker into a single call.
|
|
98
|
-
*
|
|
99
|
-
* @example
|
|
100
|
-
* ```typescript
|
|
101
|
-
* import * as firestoreTriggers from "firebase-functions/v2/firestore";
|
|
102
|
-
* import * as pubsubHandler from "firebase-functions/v2/pubsub";
|
|
103
|
-
* import { PubSub } from "@google-cloud/pubsub";
|
|
104
|
-
*
|
|
105
|
-
* const sync = createFirestoreSync(repos, {
|
|
106
|
-
* deps: { firestoreTriggers, pubsubHandler, pubsub: new PubSub() },
|
|
107
|
-
* adapter,
|
|
108
|
-
* topicPrefix: "firestore-sync",
|
|
109
|
-
* autoMigrate: true,
|
|
110
|
-
* admin: {
|
|
111
|
-
* auth: { type: "basic", username: "admin", password: "secret" },
|
|
112
|
-
* featuresFlag: { healthCheck: true, manualSync: true, viewQueue: true },
|
|
113
|
-
* },
|
|
114
|
-
* repos: {
|
|
115
|
-
* users: { exclude: ["documentPath"], columnMap: { docId: "user_id" } },
|
|
116
|
-
* posts: { columnMap: { docId: "post_id" } },
|
|
117
|
-
* },
|
|
118
|
-
* });
|
|
119
|
-
*
|
|
120
|
-
* // Triggers + PubSub handlers
|
|
121
|
-
* export const { users_onCreate, users_onUpdate, users_onDelete, sync_users } = sync.functions;
|
|
122
|
-
*
|
|
123
|
-
* // Admin endpoint — wrap with onRequest yourself
|
|
124
|
-
* export const adminsync = onRequest(sync.adminHandler!);
|
|
125
|
-
*
|
|
126
|
-
* // Or pass onRequest in admin config to auto-add to sync.functions:
|
|
127
|
-
* // admin: { onRequest, ... } → export const { adminsync } = sync.functions;
|
|
128
|
-
* ```
|
|
129
|
-
*/
|
|
130
|
-
|
|
131
|
-
declare function createFirestoreSync<M extends Record<string, any>>(repoMapping: M, config: FirestoreSyncConfig<NoInfer<M>>): {
|
|
132
|
-
/** All Cloud Functions (triggers + handlers + optional admin) — spread into exports */
|
|
133
|
-
functions: Record<string, any>;
|
|
134
|
-
/**
|
|
135
|
-
* Raw admin HTTP handler — wrap with `onRequest()` yourself if you
|
|
136
|
-
* didn't pass `onRequest` in the admin config.
|
|
137
|
-
* @example
|
|
138
|
-
* ```ts
|
|
139
|
-
* export const adminsync = onRequest(sync.adminHandler!);
|
|
140
|
-
* ```
|
|
141
|
-
*/
|
|
142
|
-
adminHandler: ((req: any, res: any) => Promise<void>) | null;
|
|
143
|
-
/** Process a SyncEvent directly (for testing) */
|
|
144
|
-
handleMessage: (event: SyncEvent) => Promise<void>;
|
|
145
|
-
/** Internal queue map (for testing) */
|
|
146
|
-
queues: Map<string, SyncQueue>;
|
|
147
|
-
/** Flush all queues and stop timers */
|
|
148
|
-
shutdown: () => Promise<void>;
|
|
149
|
-
};
|
|
150
|
-
|
|
151
49
|
/**
|
|
152
50
|
* DDL generator — produces CREATE TABLE / ALTER TABLE statements from
|
|
153
51
|
* SqlColumn definitions and a SqlDialect.
|
|
@@ -308,4 +206,4 @@ declare function createSyncWorker<M extends Record<string, any>>(repoMapping: M,
|
|
|
308
206
|
shutdown(): Promise<void>;
|
|
309
207
|
};
|
|
310
208
|
|
|
311
|
-
export {
|
|
209
|
+
export { GenerateDDLConfig, LogicalType, type MigrateResult, PubSubClientDep, RepoSyncConfig, SqlAdapter, SqlColumn, SqlDialect, SqlTableDef, SyncEvent, SyncQueue, SyncTriggersConfig, SyncWorkerConfig, addColumnsDDL, adminsyncConfig, autoMigrate, createSyncTriggers, createSyncWorker, createTableDDL, createadminsyncServer, generateDDL, serializeDocument, serializeValue, zodSchemaToColumns, zodTypeToLogical };
|
package/dist/sync/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function
|
|
1
|
+
function ee(o){let e=[],t=o.replace(/[.*+?^${}()|[\]\\]/g,s=>s===":"?s:`\\${s}`).replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g,(s,a)=>(e.push(a),"([^/]+)"));return {pattern:new RegExp(`^${t}$`),paramNames:e}}function te(o){let e=o.path??o.url??"/",t=e.indexOf("?");return t===-1?e:e.slice(0,t)}var L=class{constructor(){this.routes=[];this.middlewares=[];this.notFoundHandler=(e,t)=>{t.status(404).send("Not Found");};this.errorHandler=(e,t,s)=>{console.error("[MiniRouter]",e),s.status(500).send("Internal Server Error");};}use(e){return this.middlewares.push(e),this}get(e,t){return this.addRoute("GET",e,t)}post(e,t){return this.addRoute("POST",e,t)}put(e,t){return this.addRoute("PUT",e,t)}patch(e,t){return this.addRoute("PATCH",e,t)}delete(e,t){return this.addRoute("DELETE",e,t)}onNotFound(e){return this.notFoundHandler=e,this}onError(e){return this.errorHandler=e,this}addRoute(e,t,s){let{pattern:a,paramNames:d}=ee(t);return this.routes.push({method:e.toUpperCase(),pattern:a,paramNames:d,handler:s}),this}async handle(e,t){let s=(e.method??"GET").toUpperCase(),a=te(e),d=null,h={};for(let R of this.routes){if(R.method!==s)continue;let S=a.match(R.pattern);if(S){d=R,h={},R.paramNames.forEach((g,c)=>{h[g]=decodeURIComponent(S[c+1]??"");});break}}let w=Object.assign(e,{params:h}),l=d?d.handler:this.notFoundHandler;try{await this.runMiddlewareChain(w,t,l);}catch(R){this.errorHandler(R,e,t);}}async runMiddlewareChain(e,t,s){let a=0,d=async()=>{if(a<this.middlewares.length){let h=this.middlewares[a++];await h(e,t,d);}else await s(e,t);};await d();}};var ne={string:"ZodString",number:"ZodNumber",bigint:"ZodBigInt",boolean:"ZodBoolean",date:"ZodDate",enum:"ZodEnum",nativeEnum:"ZodNativeEnum",literal:"ZodLiteral",object:"ZodObject",array:"ZodArray",optional:"ZodOptional",nullable:"ZodNullable",default:"ZodDefault",coerce:"ZodCoerce",union:"ZodUnion",undefined:"ZodUndefined",unknown:"ZodUnknown",any:"ZodAny",record:"ZodRecord"};function U(o){let e=o,t=e._zod?.def?.type;if(t)return ne[t]??`Zod${t.charAt(0).toUpperCase()}${t.slice(1)}`;let s=e._def?.typeName;return s||""}function Q(o){let e=o;if(e._zod?.def?.innerType)return e._zod.def.innerType;if(e._def?.innerType)return e._def.innerType}function K(o){let e=o;return e.shape&&typeof e.shape=="object"?e.shape:e._zod?.def?.shape&&typeof e._zod.def.shape=="object"?e._zod.def.shape:e._def?.shape?typeof e._def.shape=="function"?e._def.shape():e._def.shape:{}}var _="__sync_version";var oe=new Set(["ZodOptional","ZodNullable","ZodDefault"]);function H(o){let e=o,t=false;for(;;){let s=U(e);if(!oe.has(s))break;(s==="ZodOptional"||s==="ZodNullable")&&(t=true);let a=Q(e);if(!a)break;e=a;}return {inner:e,nullable:t}}var G={ZodString:"string",ZodNumber:"number",ZodBigInt:"bigint",ZodBoolean:"boolean",ZodDate:"timestamp",ZodEnum:"string",ZodNativeEnum:"string",ZodLiteral:"string"};function re(o){let{inner:e}=H(o);return G[U(e)]??"json"}function V(o,e,t,s,a,d,h,w){for(let[l,R]of Object.entries(o)){let S=t?`${t}__${l}`:l;if(a.has(l)||a.has(S))continue;let{inner:g,nullable:c}=H(R),u=U(g),i=s||c;if(u==="ZodObject"){let r=K(g);V(r,e,S,i,a,d,h,w);continue}let n=G[u]??"json",p=S===h||l===h,b=d[S]??d[l]??S;w.push({name:b,sqlType:e.mapType(n),nullable:p?false:i,isPrimaryKey:p});}}function E(o,e,t={}){let{primaryKey:s,exclude:a=[],columnMap:d={}}=t,h=new Set(a),w=K(o),l=[];return V(w,e,"",false,h,d,s,l),l.some(R=>R.name===_)||l.push({name:_,sqlType:e.mapType("bigint"),nullable:true,isPrimaryKey:false,description:"Monotonic publish version (Date.now() ms). Internal."}),l}function B(o){if(o==null)return null;if(typeof o=="object"&&typeof o.toDate=="function")return o.toDate().toISOString();if(o instanceof Date)return o.toISOString();if(Buffer.isBuffer(o))return o.toString("base64");if(o instanceof Uint8Array)return Buffer.from(o).toString("base64");if(typeof o=="object"&&"latitude"in o&&"longitude"in o){let e=o;return JSON.stringify({lat:e.latitude,lng:e.longitude})}return Array.isArray(o)?JSON.stringify(o.map(B)):o}function W(o,e,t){for(let[s,a]of Object.entries(o)){let d=e?`${e}__${s}`:s;a!=null&&typeof a=="object"&&!Array.isArray(a)&&!(a instanceof Date)&&!Buffer.isBuffer(a)&&!(a instanceof Uint8Array)&&typeof a.toDate!="function"&&!("latitude"in a&&"longitude"in a)?W(a,d,t):t[d]=B(a);}}function q(o,e){let t=new Set(e?.exclude),s=e?.columnMap??{},a={};W(o,"",a);let d={};for(let[h,w]of Object.entries(a)){if(t.has(h))continue;let l=h.split("__")[0];if(l!==h&&t.has(l))continue;let R=s[h]??(h.includes("__")?s[h.split("__").pop()]:void 0)??h;d[R]=w;}return d}function z(o,e){if(process.env.FUNCTIONS_EMULATOR==="true"){let a=process.env.GCLOUD_PROJECT??process.env.GOOGLE_CLOUD_PROJECT??"demo-project",d=process.env.FUNCTION_REGION??"us-central1",h=(process.env.FUNCTION_TARGET??"").replace(/\./g,"-");return `/${a}/${d}/${h}${e}`}let t=process.env.K_SERVICE,s=o.hostname??o.headers?.host??"";return t&&s.includes("cloudfunctions.net")?`/${t.toLowerCase()}${e}`:e}function P(o,e,t){return `<!DOCTYPE html>
|
|
2
2
|
<html lang="en"><head>
|
|
3
3
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
4
4
|
<title>${o} \u2014 Sync Admin</title>
|
|
@@ -28,71 +28,71 @@ function re(o){let e=[],t=o.replace(/[.*+?^${}()|[\]\\]/g,a=>a===":"?a:`\\${a}`)
|
|
|
28
28
|
<nav><a href="${e}/">\u2190 Dashboard</a></nav>
|
|
29
29
|
<h1>${o}</h1>
|
|
30
30
|
${t}
|
|
31
|
-
</body></html>`}function
|
|
32
|
-
<td><strong>${
|
|
33
|
-
<td>${
|
|
34
|
-
<td>${
|
|
35
|
-
<td>${
|
|
31
|
+
</body></html>`}function A(o,e,t=200){o.status(t).set("Content-Type","text/html; charset=utf-8").send(e);}function Z(o,e,t=200){o.status(t).set("Content-Type","application/json").send(JSON.stringify(e,null,2));}function I(o){return (o.headers?.accept??"").includes("application/json")}function se(o,e,t,s,a,d,h,w){let l=(a.basePath??"/").replace(/\/$/,"")||"",R=a.featuresFlag??{},S=[];for(let[c,u]of Object.entries(o)){let i=d[c];S.push({name:c,schema:u.schema??null,documentKey:u._systemKeys?.[0]??u.documentKey??"docId",tableName:i?.tableName??c,isGroup:!!u._isGroup,repoCfg:i,repo:u});}let g=new L;if(a.auth)if(typeof a.auth=="function")g.use(a.auth);else {let c=a.auth.realm??"Sync Admin",u="Basic "+Buffer.from(`${a.auth.username}:${a.auth.password}`).toString("base64");g.use((i,n,p)=>{if((i.headers?.authorization??"")!==u){n.status(401).set("WWW-Authenticate",`Basic realm="${c}"`).set("Content-Type","text/plain").send("Unauthorized");return}p();});}return g.get(`${l}/`,(c,u)=>{let i=z(c,l),n=S.map(y=>{let C=[];return R.healthCheck&&C.push(`<a class="btn" href="${i}/${y.name}/health">Health</a>`),R.manualSync&&C.push(`<a class="btn btn-primary" href="${i}/${y.name}/force-sync">Force Sync</a>`),`<tr>
|
|
32
|
+
<td><strong>${y.name}</strong></td>
|
|
33
|
+
<td>${y.tableName}</td>
|
|
34
|
+
<td>${y.isGroup?'<span class="badge badge-warn">group</span>':'<span class="badge badge-ok">collection</span>'}</td>
|
|
35
|
+
<td>${y.schema?"\u2713":"\u2717"}</td>
|
|
36
36
|
<td>${C.join(" ")}</td>
|
|
37
37
|
</tr>`}).join(`
|
|
38
|
-
`),p=R.viewQueue?`<p><a class="btn" href="${i}/queues">View Queues</a></p>`:"",
|
|
38
|
+
`),p=R.viewQueue?`<p><a class="btn" href="${i}/queues">View Queues</a></p>`:"",b=R.configCheck?`<p style="margin-top:.5rem"><a class="btn" href="${i}/config-check">\u2699 Config Check</a></p>`:"",r=P("Sync Dashboard",i,`<div class="card">
|
|
39
39
|
<table>
|
|
40
40
|
<thead><tr><th>Repository</th><th>Table</th><th>Type</th><th>Schema</th><th>Actions</th></tr></thead>
|
|
41
41
|
<tbody>${n}</tbody>
|
|
42
42
|
</table>
|
|
43
43
|
${p}
|
|
44
|
-
${
|
|
45
|
-
</div>`);
|
|
46
|
-
`),v=
|
|
47
|
-
`),N=
|
|
44
|
+
${b}
|
|
45
|
+
</div>`);A(u,r);}),g.get(`${l}`,(c,u)=>{let i=z(c,l);u.status(302).set("Location",`${i}/`).send("");}),R.healthCheck&&g.get(`${l}/:repoName/health`,async(c,u)=>{let i=z(c,l),n=S.find(m=>m.name===c.params.repoName);if(!n){A(u,P("Not Found",i,`<p>Unknown repo: ${c.params.repoName}</p>`),404);return}if(!n.schema){A(u,P("Health Check",i,`<p class="badge badge-warn">No Zod schema attached to "${n.name}"</p>`));return}let p=E(n.schema,e.dialect,{primaryKey:n.documentKey,exclude:n.repoCfg?.exclude,columnMap:n.repoCfg?.columnMap}),b=[],r=false,y=null;try{r=await e.tableExists(n.tableName),r&&(b=await e.getTableColumns(n.tableName));}catch(m){y=m?.message??String(m);}let C=new Set(b),k=new Set(p.map(m=>m.name)),x=p.filter(m=>!C.has(m.name)),D=b.filter(m=>!k.has(m)),O=p.filter(m=>C.has(m.name)),f=r&&x.length===0&&!y;if(I(c)){Z(u,{repo:n.name,table:n.tableName,tableExists:r,healthy:f,error:y,columns:{expected:p.map(m=>({name:m.name,type:m.sqlType,nullable:m.nullable,isPrimaryKey:m.isPrimaryKey})),actual:b,matched:O.map(m=>m.name),missing:x.map(m=>({name:m.name,type:m.sqlType})),extra:D}});return}let $=f?'<span class="badge badge-ok">Healthy</span>':'<span class="badge badge-err">Unhealthy</span>',T=p.map(m=>{let j=C.has(m.name)?'<span class="badge badge-ok">OK</span>':'<span class="badge badge-err">MISSING</span>';return `<tr><td>${m.name}</td><td>${m.sqlType}</td><td>${m.nullable?"Yes":"No"}</td><td>${m.isPrimaryKey?"\u2713":""}</td><td>${j}</td></tr>`}).join(`
|
|
46
|
+
`),v=D.map(m=>`<tr><td>${m}</td><td colspan="3" class="muted">not in schema</td><td><span class="badge badge-warn">EXTRA</span></td></tr>`).join(`
|
|
47
|
+
`),N=P(`Health: ${n.name}`,i,`<div class="card">
|
|
48
48
|
<p>Table: <code>${n.tableName}</code> ${r?$:'<span class="badge badge-err">NOT FOUND</span>'}</p>
|
|
49
|
-
${
|
|
49
|
+
${y?`<p class="badge badge-err">Error: ${y}</p>`:""}
|
|
50
50
|
<h2>Columns</h2>
|
|
51
51
|
<table>
|
|
52
52
|
<thead><tr><th>Column</th><th>SQL Type</th><th>Nullable</th><th>PK</th><th>Status</th></tr></thead>
|
|
53
53
|
<tbody>${T}${v}</tbody>
|
|
54
54
|
</table>
|
|
55
|
-
</div>`);
|
|
55
|
+
</div>`);A(u,N);}),R.manualSync&&(g.get(`${l}/:repoName/force-sync`,(c,u)=>{let i=z(c,l),n=S.find(b=>b.name===c.params.repoName);if(!n){A(u,P("Not Found",i,`<p>Unknown repo: ${c.params.repoName}</p>`),404);return}let p=P(`Force Sync: ${n.name}`,i,`<div class="card">
|
|
56
56
|
<p>This will read <strong>all</strong> documents from the <code>${n.name}</code> Firestore collection
|
|
57
57
|
and upsert them into the <code>${n.tableName}</code> SQL table.</p>
|
|
58
58
|
<p class="muted" style="margin:.75rem 0">This may take a while for large collections.</p>
|
|
59
59
|
<form method="POST" action="${i}/${n.name}/force-sync">
|
|
60
60
|
<button type="submit" class="btn btn-primary">Start Force Sync</button>
|
|
61
61
|
</form>
|
|
62
|
-
</div>`);
|
|
63
|
-
<p class="badge badge-err">Error: ${
|
|
64
|
-
<p>Synced ${
|
|
65
|
-
</div>`),500);return}if(I(
|
|
66
|
-
<pre style="white-space:pre-wrap">${
|
|
62
|
+
</div>`);A(u,p);}),g.post(`${l}/:repoName/force-sync`,async(c,u)=>{let i=z(c,l),n=S.find(f=>f.name===c.params.repoName);if(!n){Z(u,{error:`Unknown repo: ${c.params.repoName}`},404);return}let p=n.repo.ref;if(!p){Z(u,{error:`No collection reference for "${n.name}"`},400);return}let b=0,r=0,y=[],C=500,k=p.limit(C),x=null;try{for(;;){let T=await(x?k.startAfter(x):k).get();if(T.empty)break;for(let v of T.docs){let N=v.data(),m=String(N[n.documentKey]??v.id),j=q(N,{exclude:n.repoCfg?.exclude,columnMap:n.repoCfg?.columnMap});try{await s({operation:"UPSERT",repoName:n.name,docId:m,data:j,timestamp:new Date().toISOString()}),b++;}catch(F){r++;let X=F?.message??String(F);console.error(`[ForceSync:${n.name}] doc=${m} failed:`,F),y.length<5&&y.push(`${m}: ${X}`);}}if(x=T.docs[T.docs.length-1],T.docs.length<C)break}let f=t.get(n.name);f&&await f.flush();}catch(f){if(I(c)){Z(u,{error:f?.message??String(f),synced:b,errors:r},500);return}A(u,P(`Force Sync: ${n.name}`,i,`<div class="card">
|
|
63
|
+
<p class="badge badge-err">Error: ${f?.message??String(f)}</p>
|
|
64
|
+
<p>Synced ${b} docs before failure (${r} errors).</p>
|
|
65
|
+
</div>`),500);return}if(I(c)){Z(u,{repo:n.name,table:n.tableName,synced:b,errors:r,...y.length>0&&{errorSamples:y}});return}let D=y.length>0?`<details style="margin-top:1rem"><summary>First ${y.length} error(s)</summary>
|
|
66
|
+
<pre style="white-space:pre-wrap">${y.map(f=>f.replace(/[<>&]/g,$=>`&#${$.charCodeAt(0)};`)).join(`
|
|
67
67
|
|
|
68
|
-
`)}</pre></details>`:"",
|
|
68
|
+
`)}</pre></details>`:"",O=P(`Force Sync: ${n.name}`,i,`<div class="card">
|
|
69
69
|
<p class="badge ${r>0?"badge-warn":"badge-ok"}">${r>0?"Completed with errors":"Complete"}</p>
|
|
70
|
-
<p>Synced <strong>${
|
|
70
|
+
<p>Synced <strong>${b}</strong> documents to <code>${n.tableName}</code>.</p>
|
|
71
71
|
${r>0?`<p class="badge badge-warn">${r} error(s)</p>`:""}
|
|
72
|
-
${
|
|
73
|
-
</div>`);
|
|
74
|
-
`),
|
|
72
|
+
${D}
|
|
73
|
+
</div>`);A(u,O);})),R.viewQueue&&g.get(`${l}/queues`,(c,u)=>{let i=z(c,l),n=[];for(let r of S){let y=t.get(r.name);n.push({repo:r.name,table:r.tableName,pending:y?y.size:0});}if(I(c)){Z(u,{queues:n});return}let p=n.map(r=>`<tr><td>${r.repo}</td><td>${r.table}</td><td>${r.pending===0?'<span class="badge badge-ok">0</span>':`<span class="badge badge-warn">${r.pending}</span>`}</td></tr>`).join(`
|
|
74
|
+
`),b=P("Sync Queues",i,`<div class="card">
|
|
75
75
|
<table>
|
|
76
76
|
<thead><tr><th>Repository</th><th>Table</th><th>Pending</th></tr></thead>
|
|
77
77
|
<tbody>${p}</tbody>
|
|
78
78
|
</table>
|
|
79
|
-
</div>`);
|
|
80
|
-
`),console:`${p}/iam-admin/iam?project=${n}`}}):j?r.push({name:"BigQuery Dataset",category:"bigquery",status:"error",message:`Dataset not found: ${$}`,fix:{hint:"Create the dataset first",gcloud:`bq mk --dataset ${n}:YOUR_DATASET_ID`,console:`${p}/bigquery?project=${n}`}}):r.push({name:"BigQuery API",category:"bigquery",status:"ok",message:"BigQuery API is reachable (table lookup returned expected error)"});}for(let
|
|
81
|
-
<td>${
|
|
79
|
+
</div>`);A(u,b);}),R.configCheck&&g.get(`${l}/config-check`,async(c,u)=>{let i=z(c,l),n=process.env.GCLOUD_PROJECT??process.env.GOOGLE_CLOUD_PROJECT??process.env.GCP_PROJECT??"unknown",p="https://console.cloud.google.com",b=w??"firestore-sync",r=[];try{await e.tableExists("__nonexistent_health_check__"),r.push({name:"BigQuery API",category:"bigquery",status:"ok",message:"BigQuery API is reachable"});}catch(f){let $=f?.message??String(f),T=$.toLowerCase(),v=T.includes("disabled")||T.includes("has not been used")||T.includes("accessnotconfigured"),N=T.includes("permission")||$.includes("403")||T.includes("access denied"),m=T.includes("project")&&T.includes("not found"),j=T.includes("not found")||$.includes("404");v?r.push({name:"BigQuery API",category:"bigquery",status:"error",message:"BigQuery API is not enabled",fix:{gcloud:`gcloud services enable bigquery.googleapis.com --project=${n}`,console:`${p}/apis/library/bigquery.googleapis.com?project=${n}`}}):m?r.push({name:"BigQuery Project",category:"bigquery",status:"error",message:$,fix:{hint:"The GCP project does not exist or the credentials don't have access to it. In the Firebase emulator, GCLOUD_PROJECT may override the configured projectId. Ensure you pass the correct projectId to the BigQuery constructor and have valid credentials.",console:`${p}/home/dashboard`}}):N?r.push({name:"BigQuery API",category:"bigquery",status:"error",message:`Permission denied: ${$}`,fix:{hint:"Grant the service account BigQuery Data Editor + BigQuery Job User roles",gcloud:[`SA=$(gcloud run services describe YOUR_SERVICE --region=YOUR_REGION --format="value(spec.template.spec.serviceAccountName)" --project=${n})`,`gcloud projects add-iam-policy-binding ${n} --member="serviceAccount:$SA" --role="roles/bigquery.dataEditor"`,`gcloud projects add-iam-policy-binding ${n} --member="serviceAccount:$SA" --role="roles/bigquery.jobUser"`].join(`
|
|
80
|
+
`),console:`${p}/iam-admin/iam?project=${n}`}}):j?r.push({name:"BigQuery Dataset",category:"bigquery",status:"error",message:`Dataset not found: ${$}`,fix:{hint:"Create the dataset first",gcloud:`bq mk --dataset ${n}:YOUR_DATASET_ID`,console:`${p}/bigquery?project=${n}`}}):r.push({name:"BigQuery API",category:"bigquery",status:"ok",message:"BigQuery API is reachable (table lookup returned expected error)"});}for(let f of S)try{let $=await e.tableExists(f.tableName);r.push({name:`Table: ${f.tableName}`,category:"bigquery",status:$?"ok":"warn",message:$?`Table \`${f.tableName}\` exists`:`Table \`${f.tableName}\` does not exist yet`,...!$&&{fix:{hint:"Table will be auto-created on first sync if autoMigrate is enabled. Or create it manually."}}});}catch($){r.push({name:`Table: ${f.tableName}`,category:"bigquery",status:"error",message:$?.message??String($)});}if(h)for(let f of S){let $=`${b}-${f.name}`;try{let T=h.topic($);if(typeof T.exists=="function"){let[v]=await T.exists();r.push({name:`Topic: ${$}`,category:"pubsub",status:v?"ok":"error",message:v?`Topic \`${$}\` exists`:`Topic \`${$}\` does not exist`,...!v&&{fix:{gcloud:`gcloud pubsub topics create ${$} --project=${n}`,console:`${p}/cloudpubsub/topic/list?project=${n}`}}});}else r.push({name:`Topic: ${$}`,category:"pubsub",status:"warn",message:"Cannot verify topic existence (PubSub client doesn't expose .exists())",fix:{gcloud:`gcloud pubsub topics create ${$} --project=${n}`,console:`${p}/cloudpubsub/topic/list?project=${n}`,hint:"Ensure the topic exists. It is auto-created by the Firebase emulator but must exist in production."}});}catch(T){let v=T?.message??String(T),N=v.includes("disabled")||v.includes("has not been used");if(r.push({name:N?"Pub/Sub API":`Topic: ${$}`,category:"pubsub",status:"error",message:N?"Pub/Sub API is not enabled":v,fix:N?{gcloud:`gcloud services enable pubsub.googleapis.com --project=${n}`,console:`${p}/apis/library/pubsub.googleapis.com?project=${n}`}:{gcloud:`gcloud pubsub topics create ${$} --project=${n}`,console:`${p}/cloudpubsub/topic/list?project=${n}`}}),N)break}}else r.push({name:"Pub/Sub Client",category:"pubsub",status:"warn",message:"PubSub client not available for config check"});if(I(c)){let f=r.every($=>$.status==="ok");Z(u,{project:n,healthy:f,checks:r});return}let y=f=>f==="ok"?'<span class="badge badge-ok">OK</span>':f==="warn"?'<span class="badge badge-warn">WARN</span>':'<span class="badge badge-err">ERROR</span>',C={bigquery:r.filter(f=>f.category==="bigquery"),pubsub:r.filter(f=>f.category==="pubsub"),firestore:r.filter(f=>f.category==="firestore")},k=(f,$)=>{if($.length===0)return "";let T=$.map(v=>{let N="";if(v.fix){let m=[];v.fix.hint&&m.push(`<p class="muted">${v.fix.hint}</p>`),v.fix.gcloud&&m.push(`<pre>$ ${v.fix.gcloud}</pre>`),v.fix.console&&m.push(`<p><a href="${v.fix.console}" target="_blank">Open GCP Console \u2192</a></p>`),N=`<div style="margin-top:.5rem">${m.join("")}</div>`;}return `<tr>
|
|
81
|
+
<td>${y(v.status)}</td>
|
|
82
82
|
<td><strong>${v.name}</strong><br><span class="muted">${v.message}</span>${N}</td>
|
|
83
83
|
</tr>`}).join(`
|
|
84
|
-
`);return `<h2>${
|
|
84
|
+
`);return `<h2>${f}</h2>
|
|
85
85
|
<table><thead><tr><th style="width:80px">Status</th><th>Check</th></tr></thead>
|
|
86
|
-
<tbody>${T}</tbody></table>`},
|
|
87
|
-
<p>Project: <code>${n}</code> ${
|
|
86
|
+
<tbody>${T}</tbody></table>`},D=r.every(f=>f.status==="ok")?'<span class="badge badge-ok">All checks passed</span>':'<span class="badge badge-warn">Some issues found</span>',O=P("Config Check",i,`<div class="card">
|
|
87
|
+
<p>Project: <code>${n}</code> ${D}</p>
|
|
88
88
|
${k("BigQuery",C.bigquery)}
|
|
89
89
|
${k("Pub/Sub",C.pubsub)}
|
|
90
90
|
${k("Firestore",C.firestore)}
|
|
91
|
-
</div>`);
|
|
91
|
+
</div>`);A(u,O);}),async(c,u)=>{await g.handle(c,u);}}function J(o,e){let t=e.columns.map(s=>{let a=s.isPrimaryKey?" NOT NULL":"";return ` ${o.quoteIdentifier(s.name)} ${s.sqlType}${a}`}).join(`,
|
|
92
92
|
`);return `CREATE TABLE IF NOT EXISTS ${o.quoteIdentifier(e.tableName)} (
|
|
93
93
|
${t}
|
|
94
|
-
);`}function
|
|
95
|
-
`)}function
|
|
94
|
+
);`}function ae(o,e,t){return t.map(s=>`ALTER TABLE ${o.quoteIdentifier(e)} ADD COLUMN ${o.quoteIdentifier(s.name)} ${s.sqlType};`).join(`
|
|
95
|
+
`)}function ie(o,e,t){let s=[];for(let[a,d]of Object.entries(o)){let h=d.schema??d._schema??void 0;if(!h)continue;let w=t?.repos?.[a],l=w?.tableName??a,R=d._systemKeys?.[0]??d.documentKey??"docId",S=E(h,e,{primaryKey:R,exclude:w?.exclude,columnMap:w?.columnMap}),g={tableName:l,columns:S};s.push(J(e,g));}return s.join(`
|
|
96
96
|
|
|
97
|
-
`)}async function
|
|
97
|
+
`)}async function ce(o,e,t){let s={created:[],altered:[],upToDate:[],skipped:[]};for(let[a,d]of Object.entries(o)){let h=d.schema??void 0;if(!h){s.skipped.push(a);continue}let w=t?.repos?.[a],l=w?.tableName??a,R=d._systemKeys?.[0]??d.documentKey??"docId",S=E(h,e.dialect,{primaryKey:R,exclude:w?.exclude,columnMap:w?.columnMap}),g={tableName:l,columns:S};if(!await e.tableExists(l))await e.createTable(g),s.created.push(l);else {let u=new Set(await e.getTableColumns(l)),i=S.filter(n=>!u.has(n.name));i.length>0?(await e.addColumns(l,i),s.altered.push(l)):s.upToDate.push(l);}}return s}var M=class{constructor(e){this.buffer=[];this.flushing=false;this.timer=null;this.adapter=e.adapter,this.tableName=e.tableName,this.primaryKey=e.primaryKey,this.batchSize=e.batchSize??100,this.onFlushError=e.onFlushError;let t=e.flushIntervalMs??5e3;t>0&&(this.timer=setInterval(()=>{this.flush();},t),typeof this.timer=="object"&&"unref"in this.timer&&this.timer.unref());}get size(){return this.buffer.length}enqueue(...e){this.buffer.push(...e),this.buffer.length>=this.batchSize&&this.flush();}async flush(){if(this.flushing||this.buffer.length===0)return;this.flushing=true;let e=this.buffer.splice(0,this.batchSize);try{let t=new Map,s=[];for(let d of e)if(d.operation==="DELETE")s.push(d.docId),t.delete(d.docId);else if(d.data){let h=t.get(d.docId);if(!h)t.set(d.docId,d.data);else {let w=Number(h[_]??0);Number(d.data[_]??0)>=w&&t.set(d.docId,d.data);}}let a=Array.from(t.values());a.length>0&&await this.adapter.upsertRows(this.tableName,a,this.primaryKey),s.length>0&&await this.adapter.deleteRows(this.tableName,this.primaryKey,s);}catch(t){this.onFlushError?await this.onFlushError(e,t).catch(s=>{console.error(`[SyncQueue] Flush error for ${this.tableName}:`,t),console.error("[SyncQueue] Error handler also failed:",s);}):(this.buffer.unshift(...e),console.error(`[SyncQueue] Flush failed for ${this.tableName}:`,t));}finally{this.flushing=false;}}async shutdown(){this.timer&&(clearInterval(this.timer),this.timer=null),await this.flush();}};var de="firestore-sync";function ue(o,e){let t=e.ref?.path??void 0;return t?`${t}/{docId}`:(console.warn(`[SyncTriggers] Cannot determine collection path for "${o}". Skipping.`),null)}function le(o,e){let{onDocumentCreated:t,onDocumentUpdated:s,onDocumentDeleted:a}=e.deps.firestoreTriggers,d=e.deps.pubsub,h=e?.topicPrefix??de,w={},l=new Map;function R(g){let c=l.get(g);return c||(c=d.topic(g),l.set(g,c),c)}async function S(g,c){await R(g).publishMessage({json:c});}for(let[g,c]of Object.entries(o)){let u=e?.repos?.[g],i;if(c._isGroup){if(!u?.triggerPath){console.warn(`[SyncTriggers] Skipping collection-group repo "${g}". Provide a triggerPath in the sync repos config for group collections.`);continue}i=u.triggerPath;}else i=u?.triggerPath??ue(g,c);if(!i)continue;let n=c._systemKeys?.[0]??"docId",p=`${h}-${g}`;w[`${g}_onCreate`]=t(i,async b=>{let r=b.data;if(!r)return;let y=r.data();if(!y)return;let C=String(y[n]??r.id),k=q(y,{exclude:u?.exclude,columnMap:u?.columnMap}),x={operation:"INSERT",repoName:g,docId:C,data:k,timestamp:new Date().toISOString(),version:Date.now()};await S(p,x);}),w[`${g}_onUpdate`]=s(i,async b=>{let r=b.data?.after;if(!r)return;let y=r.data();if(!y)return;let C=String(y[n]??r.id),k=q(y,{exclude:u?.exclude,columnMap:u?.columnMap}),x={operation:"UPSERT",repoName:g,docId:C,data:k,timestamp:new Date().toISOString(),version:Date.now()};await S(p,x);}),w[`${g}_onDelete`]=a(i,async b=>{let r=b.data;if(!r)return;let y=r.data(),C=String(y?.[n]??r.id),k={operation:"DELETE",repoName:g,docId:C,data:null,timestamp:new Date().toISOString(),version:Date.now()};await S(p,k);});}return w}var Y=new Set;async function pe(o,e,t,s,a,d,h){if(Y.has(o))return;let w=E(t,e.dialect,{primaryKey:a,exclude:d,columnMap:h});if(!await e.tableExists(s))await e.createTable({tableName:s,columns:w});else {let R=new Set(await e.getTableColumns(s)),S=w.filter(g=>!R.has(g.name));S.length>0&&await e.addColumns(s,S);}Y.add(o);}function fe(o,e){let{deps:t,adapter:s,batchSize:a=100,flushIntervalMs:d=5e3,autoMigrate:h=false,topicPrefix:w="firestore-sync",workerOptions:l,repos:R={}}=e,S=new Map;function g(i,n){let p=S.get(i);if(p)return p;let r=R[i]?.tableName??i,y=async(C,k)=>{console.error(`[SyncWorker] Flush failed for "${i}" (${C.length} events):`,k);try{let x=`${w}-${i}-dlq`,D=t.pubsub.topic(x),[O]=await D.exists();O||(await D.create(),console.info(`[SyncWorker] Created DLQ topic "${x}"`));for(let f of C)await D.publishMessage({json:f});}catch(x){console.error(`[SyncWorker] Dead-letter publish also failed for ${i}:`,x);}};return p=new M({adapter:s,tableName:r,primaryKey:n,batchSize:a,flushIntervalMs:d,onFlushError:y}),S.set(i,p),p}async function c(i){let{repoName:n}=i,p=o[n];if(!p){console.warn(`[SyncWorker] Unknown repo "${n}", skipping event`);return}let b=p._systemKeys?.[0]??p.documentKey??"docId",r=R[n],y=r?.columnMap,C=y?.[b]??b;if(h){let x=p.schema??void 0;if(x){let D=r?.tableName??n;await pe(n,s,x,D,b,r?.exclude,y);}}let k=g(n,C);i.data&&(i.data[_]=i.version??Date.now()),k.enqueue(i);}function u(i){let n=async p=>{let b=p.data?.message?.json??p.data?.json;if(!b){console.warn("[SyncWorker] Received empty PubSub message");return}await c(b);let r=S.get(b.repoName);r&&await r.flush();};return l?t.pubsubHandler.onMessagePublished({topic:i,...l},n):t.pubsubHandler.onMessagePublished(i,n)}return {handleMessage:c,createHandler:u,queues:S,async shutdown(){let i=[];for(let n of S.values())i.push(n.shutdown());await Promise.all(i);}}}export{M as SyncQueue,ae as addColumnsDDL,ce as autoMigrate,le as createSyncTriggers,fe as createSyncWorker,J as createTableDDL,se as createadminsyncServer,ie as generateDDL,q as serializeDocument,B as serializeValue,E as zodSchemaToColumns,re as zodTypeToLogical};//# sourceMappingURL=index.js.map
|
|
98
98
|
//# sourceMappingURL=index.js.map
|