@causa/workspace-google 0.15.0 → 0.17.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.
Files changed (49) hide show
  1. package/README.md +12 -1
  2. package/dist/configurations/generated.d.ts +6 -0
  3. package/dist/configurations/generated.js +11 -0
  4. package/dist/configurations/schemas/google.yaml +10 -0
  5. package/dist/functions/event-topic/broker-create-trigger-cloud-run.call.d.ts +1 -1
  6. package/dist/functions/event-topic/broker-create-trigger-cloud-run.call.js +41 -11
  7. package/dist/functions/event-topic/broker-create-trigger-cloud-run.d.ts +11 -2
  8. package/dist/functions/event-topic/broker-create-trigger-cloud-run.js +18 -6
  9. package/dist/functions/event-topic/broker-publish-events-google.call.js +42 -26
  10. package/dist/functions/event-topic/create-backfill-source-bigquery.call.d.ts +4 -0
  11. package/dist/functions/event-topic/create-backfill-source-bigquery.call.js +48 -0
  12. package/dist/functions/event-topic/create-backfill-source-bigquery.d.ts +20 -0
  13. package/dist/functions/event-topic/create-backfill-source-bigquery.js +32 -0
  14. package/dist/functions/event-topic/index.d.ts +2 -0
  15. package/dist/functions/event-topic/index.js +2 -0
  16. package/dist/functions/event-topic/query-events-bigquery.call.d.ts +4 -0
  17. package/dist/functions/event-topic/query-events-bigquery.call.js +41 -0
  18. package/dist/functions/event-topic/query-events-bigquery.d.ts +10 -0
  19. package/dist/functions/event-topic/query-events-bigquery.js +17 -0
  20. package/dist/functions/google-firestore/index.d.ts +1 -0
  21. package/dist/functions/google-firestore/index.js +1 -0
  22. package/dist/functions/google-firestore/query-records.call.d.ts +3 -0
  23. package/dist/functions/google-firestore/query-records.call.js +50 -0
  24. package/dist/functions/google-firestore/query-records.d.ts +18 -0
  25. package/dist/functions/google-firestore/query-records.js +22 -0
  26. package/dist/functions/google-spanner/index.d.ts +1 -0
  27. package/dist/functions/google-spanner/index.js +1 -0
  28. package/dist/functions/google-spanner/query-records.call.d.ts +3 -0
  29. package/dist/functions/google-spanner/query-records.call.js +66 -0
  30. package/dist/functions/google-spanner/query-records.d.ts +23 -0
  31. package/dist/functions/google-spanner/query-records.js +27 -0
  32. package/dist/functions/index.js +5 -4
  33. package/dist/functions/service-container/index.d.ts +1 -0
  34. package/dist/functions/service-container/index.js +1 -0
  35. package/dist/functions/service-container/query-logs-cloud-run.call.d.ts +4 -0
  36. package/dist/functions/service-container/query-logs-cloud-run.call.js +36 -0
  37. package/dist/functions/service-container/query-logs-cloud-run.d.ts +12 -0
  38. package/dist/functions/service-container/query-logs-cloud-run.js +18 -0
  39. package/dist/services/index.d.ts +1 -0
  40. package/dist/services/index.js +1 -0
  41. package/dist/services/logging.d.ts +16 -0
  42. package/dist/services/logging.js +20 -0
  43. package/package.json +18 -17
  44. package/dist/backfilling/bigquery.d.ts +0 -53
  45. package/dist/backfilling/bigquery.js +0 -101
  46. package/dist/backfilling/index.d.ts +0 -2
  47. package/dist/backfilling/index.js +0 -2
  48. package/dist/backfilling/pubsub.d.ts +0 -34
  49. package/dist/backfilling/pubsub.js +0 -65
package/README.md CHANGED
@@ -55,10 +55,21 @@ The following emulators are implemented:
55
55
 
56
56
  ### Backfilling
57
57
 
58
- Backfilling is supported when `google.pubSub` is set as the `events.broker`. Temporary triggers can be created for Cloud Run services, by referencing them using the format `[[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger`.
58
+ Backfilling is supported when `google.pubSub` is set as the `events.broker`. Temporary triggers can be created for Cloud Run services in two ways:
59
+
60
+ - As a raw URI, using the format `[[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger`.
61
+ - As a project-scoped trigger, using the format `<projectPath>#<triggerName>[?<options>]`. The referenced project must be of type `serviceContainer` with `google.cloudRun` as `serviceContainer.platform`. The HTTP path is read from `serviceContainer.triggers[<triggerName>].endpoint` (whose `type` must be `http`), and the `options` query string is forwarded as URL query parameters. The target Cloud Run service name defaults to `project.name`, and can be overridden by setting `google.cloudRun.eventBackfillServiceName`.
59
62
 
60
63
  When no source is specified, the default is to fetch events to backfill from the BigQuery dataset configured in `google.pubSub.bigQueryStorage`. A custom BigQuery table can also be set as source using the `bq://<projectId>.<datasetId>.<tableId>` format. It should have the `data` and `attributes` columns.
61
64
 
65
+ ### Querying
66
+
67
+ This module implements the `DatabaseQueryRecords`, `ServiceContainerQueryLogs`, and `EventTopicQueryEvents` workspace functions, which expose read-only access to data stored in or emitted by deployed services:
68
+
69
+ - `DatabaseQueryRecords` is supported for the `google.spanner` and `google.firestore` engines. For Spanner, the function takes a database name and a SQL query, runs it on the configured Spanner instance, and returns the matching rows (capped to 100000). For Firestore, the query is the path to a single document, fetched from the default database (or the specified one). The document data is returned as a single-element array. An empty array is returned when the document does not exist.
70
+ - `ServiceContainerQueryLogs` is supported when `serviceContainer.platform` is `google.cloudRun`. It queries Cloud Logging for log entries emitted by the service's Cloud Run revisions. By default, only the last hour of logs and up to 1 000 entries are returned.
71
+ - `EventTopicQueryEvents` is supported when `events.broker` is `google.pubSub`, `events.format` is `json`, and `google.pubSub.bigQueryStorage.rawEventsDatasetId` is set. It reads events from the topic's BigQuery raw events table. Same defaults as for log queries: last hour and up to 1 000 events.
72
+
62
73
  ### Secrets backend
63
74
 
64
75
  This module implements the `google.secretManager` secret backend, allowing fetching secrets from the Google Secret Manager service. Here are some example of how secrets with the `google.secretManager` backend should be defined:
@@ -67,6 +67,12 @@ export declare class GoogleCloudRun {
67
67
  * The Docker repository where Cloud Run containers should be uploaded.
68
68
  */
69
69
  readonly dockerRepository?: string;
70
+ /**
71
+ * For `serviceContainer` projects deployed on Cloud Run, the name of the Cloud Run service that receives
72
+ * events during a backfill targeting a project-scoped trigger. Defaults to the project's name
73
+ * (`project.name`) when unset.
74
+ */
75
+ readonly eventBackfillServiceName?: string;
70
76
  /**
71
77
  * The type of allowed ingress that can reach the container.
72
78
  * See https://cloud.google.com/run/docs/securing/ingress#settings for more details.
@@ -110,6 +110,12 @@ export class GoogleCloudRun {
110
110
  * The Docker repository where Cloud Run containers should be uploaded.
111
111
  */
112
112
  dockerRepository;
113
+ /**
114
+ * For `serviceContainer` projects deployed on Cloud Run, the name of the Cloud Run service that receives
115
+ * events during a backfill targeting a project-scoped trigger. Defaults to the project's name
116
+ * (`project.name`) when unset.
117
+ */
118
+ eventBackfillServiceName;
113
119
  /**
114
120
  * The type of allowed ingress that can reach the container.
115
121
  * See https://cloud.google.com/run/docs/securing/ingress#settings for more details.
@@ -156,6 +162,11 @@ __decorate([
156
162
  IsString(),
157
163
  __metadata("design:type", String)
158
164
  ], GoogleCloudRun.prototype, "dockerRepository", void 0);
165
+ __decorate([
166
+ AllowMissing(),
167
+ IsString(),
168
+ __metadata("design:type", String)
169
+ ], GoogleCloudRun.prototype, "eventBackfillServiceName", void 0);
159
170
  __decorate([
160
171
  AllowMissing(),
161
172
  IsIn(['all', 'internal', 'internal-and-cloud-load-balancing']),
@@ -358,6 +358,16 @@ properties:
358
358
  location:
359
359
  type: string
360
360
  description: The location where Cloud Run services should be deployed.
361
+ eventBackfillServiceName:
362
+ type: string
363
+ description: >-
364
+ For `serviceContainer` projects deployed on Cloud Run, the name of
365
+ the Cloud Run service that receives
366
+
367
+ events during a backfill targeting a project-scoped trigger.
368
+ Defaults to the project's name
369
+
370
+ (`project.name`) when unset.
361
371
  secretEnvironmentVariables:
362
372
  type: object
363
373
  description: >-
@@ -1,3 +1,3 @@
1
1
  import type { WorkspaceContext } from '@causa/workspace';
2
- import type { EventTopicBrokerCreateTriggerForCloudRun } from './broker-create-trigger-cloud-run.js';
2
+ import { type EventTopicBrokerCreateTriggerForCloudRun } from './broker-create-trigger-cloud-run.js';
3
3
  export default function call(this: EventTopicBrokerCreateTriggerForCloudRun, context: WorkspaceContext): Promise<string[]>;
@@ -1,23 +1,53 @@
1
1
  import { CloudRunPubSubTriggerService } from '../../services/cloud-run-pubsub-trigger.js';
2
+ import { CLOUD_RUN_TRIGGER_ID_REGEX, } from './broker-create-trigger-cloud-run.js';
2
3
  /**
3
- * The regular expression used to match Cloud Run trigger IDs, consisting of the service ID and the path to the trigger.
4
- * [[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger
4
+ * Resolves the Cloud Run service ID and HTTP path for a raw URI trigger string.
5
5
  */
6
- const CLOUD_RUN_TRIGGER_ID_REGEX = /^(?:(?:projects\/(?<projectId>[\w-]+)\/)?locations\/(?<location>[\w-]+)\/)?services\/(?<name>[\w-]+)(?<path>\/.*)$/;
7
- export default async function call(context) {
8
- const match = this.trigger.match(CLOUD_RUN_TRIGGER_ID_REGEX);
6
+ function resolveFromString(context, trigger) {
7
+ const match = trigger.match(CLOUD_RUN_TRIGGER_ID_REGEX);
9
8
  if (!match?.groups) {
10
- throw new Error('Oops.');
9
+ throw new Error(`Invalid Cloud Run trigger '${trigger}'.`);
11
10
  }
12
11
  const googleConf = context.asConfiguration();
13
12
  const projectId = match.groups.projectId ?? googleConf.getOrThrow('google.project');
14
13
  const location = match.groups.location ?? googleConf.getOrThrow('google.cloudRun.location');
15
- const name = match.groups.name;
16
- const path = match.groups.path;
17
- if (!name || !path) {
18
- throw new Error('Oops.');
14
+ return {
15
+ serviceId: `projects/${projectId}/locations/${location}/services/${match.groups.name}`,
16
+ path: match.groups.path,
17
+ };
18
+ }
19
+ /**
20
+ * Resolves the Cloud Run service ID and HTTP path for a project-scoped trigger object.
21
+ * The project is expected to be a `serviceContainer` deployed on Cloud Run. The service name defaults to the project's
22
+ * name, and is overridden by `google.cloudRun.eventBackfillServiceName` when set. The trigger path is read from
23
+ * `serviceContainer.triggers[name].endpoint`, which must be of type `http`. Options are serialized as a URL query
24
+ * string.
25
+ *
26
+ * @param context The {@link WorkspaceContext}.
27
+ * @param trigger The trigger object.
28
+ * @returns The resolved service ID and path.
29
+ */
30
+ function resolveFromObject(context, trigger) {
31
+ const conf = context.asConfiguration();
32
+ const projectId = conf.getOrThrow('google.project');
33
+ const location = conf.getOrThrow('google.cloudRun.location');
34
+ const serviceName = conf.get('google.cloudRun.eventBackfillServiceName') ??
35
+ conf.getOrThrow('project.name');
36
+ const endpoint = conf.get(`serviceContainer.triggers.${trigger.name}.endpoint`);
37
+ if (!endpoint || endpoint.type !== 'http' || !endpoint.path) {
38
+ throw new Error(`Trigger '${trigger.name}' does not exist or does not define an HTTP endpoint.`);
19
39
  }
20
- const serviceId = `projects/${projectId}/locations/${location}/services/${name}`;
40
+ const query = new URLSearchParams(trigger.options).toString();
41
+ const path = query ? `${endpoint.path}?${query}` : endpoint.path;
42
+ return {
43
+ serviceId: `projects/${projectId}/locations/${location}/services/${serviceName}`,
44
+ path,
45
+ };
46
+ }
47
+ export default async function call(context) {
48
+ const { serviceId, path } = typeof this.trigger === 'string'
49
+ ? resolveFromString(context, this.trigger)
50
+ : resolveFromObject(context, this.trigger);
21
51
  return await context
22
52
  .service(CloudRunPubSubTriggerService)
23
53
  .create(this.backfillId, this.topicId, serviceId, path);
@@ -1,9 +1,18 @@
1
1
  import { WorkspaceContext } from '@causa/workspace';
2
2
  import { EventTopicBrokerCreateTrigger } from '@causa/workspace-core';
3
+ /**
4
+ * The regular expression used to match Cloud Run trigger IDs, consisting of the service ID and the path to the trigger.
5
+ * [[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger
6
+ */
7
+ export declare const CLOUD_RUN_TRIGGER_ID_REGEX: RegExp;
3
8
  /**
4
9
  * Implements {@link EventTopicBrokerCreateTrigger} for Cloud Run endpoints being triggered by Pub/Sub messages.
5
- * The `id` argument should be a Cloud Run service ID, followed by the path to the trigger, e.g.
6
- * `[[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger`.
10
+ *
11
+ * Two forms of trigger are supported:
12
+ * - A string in the format `[[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger`.
13
+ * - An object `{ name, options }` passed when the function is called on a project-scoped context. In that case the
14
+ * project must be a `serviceContainer` deployed on Cloud Run. The trigger's `name` must match an entry in
15
+ * `serviceContainer.triggers` whose endpoint is of type `http`. Options are forwarded as URL query parameters.
7
16
  */
8
17
  export declare class EventTopicBrokerCreateTriggerForCloudRun extends EventTopicBrokerCreateTrigger {
9
18
  _call(context: WorkspaceContext): Promise<string[]>;
@@ -4,19 +4,31 @@ import { EventTopicBrokerCreateTrigger, } from '@causa/workspace-core';
4
4
  * The regular expression used to match Cloud Run trigger IDs, consisting of the service ID and the path to the trigger.
5
5
  * [[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger
6
6
  */
7
- const CLOUD_RUN_TRIGGER_ID_REGEX = /^(?:(?:projects\/(?<projectId>[\w-]+)\/)?locations\/(?<location>[\w-]+)\/)?services\/(?<name>[\w-]+)(?<path>\/.*)$/;
7
+ export const CLOUD_RUN_TRIGGER_ID_REGEX = /^(?:(?:projects\/(?<projectId>[\w-]+)\/)?locations\/(?<location>[\w-]+)\/)?services\/(?<name>[\w-]+)(?<path>\/.*)$/;
8
8
  /**
9
9
  * Implements {@link EventTopicBrokerCreateTrigger} for Cloud Run endpoints being triggered by Pub/Sub messages.
10
- * The `id` argument should be a Cloud Run service ID, followed by the path to the trigger, e.g.
11
- * `[[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger`.
10
+ *
11
+ * Two forms of trigger are supported:
12
+ * - A string in the format `[[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger`.
13
+ * - An object `{ name, options }` passed when the function is called on a project-scoped context. In that case the
14
+ * project must be a `serviceContainer` deployed on Cloud Run. The trigger's `name` must match an entry in
15
+ * `serviceContainer.triggers` whose endpoint is of type `http`. Options are forwarded as URL query parameters.
12
16
  */
13
17
  export class EventTopicBrokerCreateTriggerForCloudRun extends EventTopicBrokerCreateTrigger {
14
18
  async _call(context) {
15
19
  return await callDeferred(this, context, import.meta.url);
16
20
  }
17
21
  _supports(context) {
18
- return (this.trigger.match(CLOUD_RUN_TRIGGER_ID_REGEX) != null &&
19
- context.asConfiguration().get('events.broker') ===
20
- 'google.pubSub');
22
+ const isPubSub = context.asConfiguration().get('events.broker') ===
23
+ 'google.pubSub';
24
+ if (!isPubSub) {
25
+ return false;
26
+ }
27
+ if (typeof this.trigger === 'string') {
28
+ return CLOUD_RUN_TRIGGER_ID_REGEX.test(this.trigger);
29
+ }
30
+ const conf = context.asConfiguration();
31
+ return (conf.get('project.type') === 'serviceContainer' &&
32
+ conf.get('serviceContainer.platform') === 'google.cloudRun');
21
33
  }
22
34
  }
@@ -1,29 +1,45 @@
1
- import { JsonFilesEventSource, } from '@causa/workspace-core/backfill';
2
- import { BigQueryEventsSource, PubSubBackfillEventPublisher, } from '../../backfilling/index.js';
3
- async function createSource(self, context) {
4
- if (!self.source) {
5
- const googleConf = context.asConfiguration();
6
- const projectId = googleConf.getOrThrow('google.project');
7
- const rawEventsDatasetId = googleConf.get('google.pubSub.bigQueryStorage.rawEventsDatasetId');
8
- if (!rawEventsDatasetId) {
9
- throw new Error('Cannot use the default event source because BigQuery storage is not configured.');
1
+ import { FlowControlledPublisher } from '@google-cloud/pubsub/build/src/publisher/flow-publisher.js';
2
+ import { PubSubService } from '../../services/index.js';
3
+ /**
4
+ * The number of events between each progress log line.
5
+ */
6
+ const PUBLISH_PROGRESS_LOG_INTERVAL = 10000;
7
+ /**
8
+ * Options when publishing events in batches to Pub/Sub.
9
+ */
10
+ const PUBLISH_OPTIONS = {
11
+ flowControlOptions: {
12
+ maxOutstandingBytes: 10 * 1024 * 1024,
13
+ maxOutstandingMessages: 1000,
14
+ },
15
+ batching: {
16
+ maxBytes: 10 * 1024 * 1024,
17
+ maxMessages: 1000,
18
+ maxMilliseconds: 1000,
19
+ },
20
+ };
21
+ export default async function call(context) {
22
+ const publisher = context
23
+ .service(PubSubService)
24
+ .pubSub.topic(this.topicId, PUBLISH_OPTIONS)
25
+ .flowControlled();
26
+ context.logger.info('📫 Publishing events.');
27
+ let numEvents = 0;
28
+ for await (const event of this.source()) {
29
+ numEvents += 1;
30
+ const wait = publisher.publish({
31
+ data: event.data,
32
+ attributes: event.attributes,
33
+ orderingKey: event.key,
34
+ });
35
+ if (wait) {
36
+ context.logger.debug('Waiting for the publisher to catch up before resuming publishing.');
37
+ await wait;
38
+ }
39
+ if (numEvents % PUBLISH_PROGRESS_LOG_INTERVAL === 0) {
40
+ context.logger.debug(`Published ${numEvents} events.`);
10
41
  }
11
- const tableName = self.eventTopic.replace(/[-\.]/g, '_');
12
- const tableId = `${projectId}.${rawEventsDatasetId}.${tableName}`;
13
- return new BigQueryEventsSource(context, tableId);
14
- }
15
- const bqSource = await BigQueryEventsSource.fromSourceAndFilter(context, self.source, self.filter);
16
- if (bqSource) {
17
- return bqSource;
18
- }
19
- const jsonFilesSource = await JsonFilesEventSource.fromSourceAndFilter(context, self.source, self.filter);
20
- if (jsonFilesSource) {
21
- return jsonFilesSource;
22
42
  }
23
- throw new Error(`The event source '${self.source}' is not supported.`);
24
- }
25
- export default async function call(context) {
26
- const source = await createSource(this, context);
27
- const publisher = new PubSubBackfillEventPublisher(context, this.topicId);
28
- await publisher.publishFromSource(source);
43
+ await publisher.all();
44
+ context.logger.info(`📫 Finished publishing ${numEvents} events.`);
29
45
  }
@@ -0,0 +1,4 @@
1
+ import type { WorkspaceContext } from '@causa/workspace';
2
+ import type { BackfillEvent } from '@causa/workspace-core';
3
+ import { type EventTopicCreateBackfillSourceForBigQuery } from './create-backfill-source-bigquery.js';
4
+ export default function call(this: EventTopicCreateBackfillSourceForBigQuery, context: WorkspaceContext): Promise<AsyncIterable<BackfillEvent>>;
@@ -0,0 +1,48 @@
1
+ import { BigQueryService } from '../../services/index.js';
2
+ import { BIGQUERY_SOURCE_REGEX, } from './create-backfill-source-bigquery.js';
3
+ /**
4
+ * Resolves the full BigQuery table ID from the function arguments and configuration.
5
+ *
6
+ * @param self The function instance.
7
+ * @param context The {@link WorkspaceContext}.
8
+ * @returns The full BigQuery table ID, in the format `projectId.datasetId.tableId`.
9
+ */
10
+ function resolveTableId(self, context) {
11
+ if (self.source) {
12
+ const match = self.source.match(BIGQUERY_SOURCE_REGEX);
13
+ if (!match?.groups?.tableId) {
14
+ throw new Error(`The source '${self.source}' is not a valid BigQuery source.`);
15
+ }
16
+ return match.groups.tableId;
17
+ }
18
+ const googleConf = context.asConfiguration();
19
+ const projectId = googleConf.getOrThrow('google.project');
20
+ const rawEventsDatasetId = googleConf.getOrThrow('google.pubSub.bigQueryStorage.rawEventsDatasetId');
21
+ const tableName = self.eventTopic.replace(/[-\.]/g, '_');
22
+ return `${projectId}.${rawEventsDatasetId}.${tableName}`;
23
+ }
24
+ export default async function call(context) {
25
+ const tableId = resolveTableId(this, context);
26
+ let query = `
27
+ SELECT
28
+ data,
29
+ attributes,
30
+ FROM
31
+ \`${tableId}\``;
32
+ if (this.filter) {
33
+ query += `
34
+ WHERE
35
+ (${this.filter})`;
36
+ }
37
+ context.logger.debug(`Creating BigQuery job from query '${query}'.`);
38
+ const bigQuery = context.service(BigQueryService).bigQuery;
39
+ const [job] = await bigQuery.createQueryJob(query);
40
+ return (async function* () {
41
+ for await (const row of job.getQueryResultsStream()) {
42
+ yield {
43
+ data: Buffer.from(row.data),
44
+ attributes: JSON.parse(row.attributes),
45
+ };
46
+ }
47
+ })();
48
+ }
@@ -0,0 +1,20 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ import { type BackfillEvent, EventTopicCreateBackfillSource } from '@causa/workspace-core';
3
+ /**
4
+ * The regular expression used to parse a `bq://<tableId>` source string.
5
+ */
6
+ export declare const BIGQUERY_SOURCE_REGEX: RegExp;
7
+ /**
8
+ * Implements {@link EventTopicCreateBackfillSource} for BigQuery tables.
9
+ *
10
+ * Two cases are supported:
11
+ * - The `source` is a `bq://<projectId>.<datasetId>.<tableId>` URI.
12
+ * - The `source` is omitted and the broker is `google.pubSub` with `google.pubSub.bigQueryStorage.rawEventsDatasetId`
13
+ * configured: events are read from the default BigQuery storage for the topic.
14
+ *
15
+ * The returned async iterable pages through the query results and yields each row as a {@link BackfillEvent}.
16
+ */
17
+ export declare class EventTopicCreateBackfillSourceForBigQuery extends EventTopicCreateBackfillSource {
18
+ _call(context: WorkspaceContext): Promise<AsyncIterable<BackfillEvent>>;
19
+ _supports(context: WorkspaceContext): boolean;
20
+ }
@@ -0,0 +1,32 @@
1
+ import { callDeferred, WorkspaceContext } from '@causa/workspace';
2
+ import { EventTopicCreateBackfillSource, } from '@causa/workspace-core';
3
+ /**
4
+ * The regular expression used to parse a `bq://<tableId>` source string.
5
+ */
6
+ export const BIGQUERY_SOURCE_REGEX = /^bq:\/\/(?<tableId>.+)$/;
7
+ /**
8
+ * Implements {@link EventTopicCreateBackfillSource} for BigQuery tables.
9
+ *
10
+ * Two cases are supported:
11
+ * - The `source` is a `bq://<projectId>.<datasetId>.<tableId>` URI.
12
+ * - The `source` is omitted and the broker is `google.pubSub` with `google.pubSub.bigQueryStorage.rawEventsDatasetId`
13
+ * configured: events are read from the default BigQuery storage for the topic.
14
+ *
15
+ * The returned async iterable pages through the query results and yields each row as a {@link BackfillEvent}.
16
+ */
17
+ export class EventTopicCreateBackfillSourceForBigQuery extends EventTopicCreateBackfillSource {
18
+ async _call(context) {
19
+ return await callDeferred(this, context, import.meta.url);
20
+ }
21
+ _supports(context) {
22
+ if (this.source?.match(BIGQUERY_SOURCE_REGEX)) {
23
+ return true;
24
+ }
25
+ if (this.source) {
26
+ return false;
27
+ }
28
+ const conf = context.asConfiguration();
29
+ return (conf.get('events.broker') === 'google.pubSub' &&
30
+ !!conf.get('google.pubSub.bigQueryStorage.rawEventsDatasetId'));
31
+ }
32
+ }
@@ -6,3 +6,5 @@ export { EventTopicBrokerDeleteTriggerResourceForPubSubSubscription } from './br
6
6
  export { EventTopicBrokerDeleteTriggerResourceForServiceAccount } from './broker-delete-trigger-resource-service-account.js';
7
7
  export { EventTopicBrokerGetTopicIdForPubSub } from './broker-get-topic-id-pubsub.js';
8
8
  export { EventTopicBrokerPublishEventsForGoogle } from './broker-publish-events-google.js';
9
+ export { EventTopicCreateBackfillSourceForBigQuery } from './create-backfill-source-bigquery.js';
10
+ export { EventTopicQueryEventsForBigQuery } from './query-events-bigquery.js';
@@ -6,3 +6,5 @@ export { EventTopicBrokerDeleteTriggerResourceForPubSubSubscription } from './br
6
6
  export { EventTopicBrokerDeleteTriggerResourceForServiceAccount } from './broker-delete-trigger-resource-service-account.js';
7
7
  export { EventTopicBrokerGetTopicIdForPubSub } from './broker-get-topic-id-pubsub.js';
8
8
  export { EventTopicBrokerPublishEventsForGoogle } from './broker-publish-events-google.js';
9
+ export { EventTopicCreateBackfillSourceForBigQuery } from './create-backfill-source-bigquery.js';
10
+ export { EventTopicQueryEventsForBigQuery } from './query-events-bigquery.js';
@@ -0,0 +1,4 @@
1
+ import type { WorkspaceContext } from '@causa/workspace';
2
+ import type { QueriedEvent } from '@causa/workspace-core';
3
+ import type { EventTopicQueryEventsForBigQuery } from './query-events-bigquery.js';
4
+ export default function call(this: EventTopicQueryEventsForBigQuery, context: WorkspaceContext): Promise<QueriedEvent[]>;
@@ -0,0 +1,41 @@
1
+ import { BigQueryService } from '../../services/index.js';
2
+ /**
3
+ * The default duration applied to `from` when it is not set, in milliseconds.
4
+ */
5
+ const DEFAULT_FROM_OFFSET = 60 * 60 * 1000;
6
+ /**
7
+ * The default value for `limit` when it is not set.
8
+ */
9
+ const DEFAULT_LIMIT = 1000;
10
+ export default async function call(context) {
11
+ const from = this.from ?? new Date(Date.now() - DEFAULT_FROM_OFFSET);
12
+ const limit = this.limit ?? DEFAULT_LIMIT;
13
+ const googleConf = context.asConfiguration();
14
+ const projectId = googleConf.getOrThrow('google.project');
15
+ const rawEventsDatasetId = googleConf.getOrThrow('google.pubSub.bigQueryStorage.rawEventsDatasetId');
16
+ const tableName = this.topic.replace(/[-\.]/g, '_');
17
+ const tableId = `${projectId}.${rawEventsDatasetId}.${tableName}`;
18
+ const conditions = [`publish_time >= @from`];
19
+ const params = { from: from.toISOString() };
20
+ if (this.to) {
21
+ conditions.push(`publish_time < @to`);
22
+ params.to = this.to.toISOString();
23
+ }
24
+ if (this.filter) {
25
+ conditions.push(`(${this.filter})`);
26
+ }
27
+ const query = `
28
+ SELECT publish_time, data, attributes
29
+ FROM \`${tableId}\`
30
+ WHERE ${conditions.join(' AND ')}
31
+ ORDER BY publish_time ASC
32
+ LIMIT ${limit}`;
33
+ context.logger.debug(`Querying BigQuery for events:\n${query}`);
34
+ const bigQuery = context.service(BigQueryService).bigQuery;
35
+ const [rows] = await bigQuery.query({ query, params });
36
+ return rows.map((row) => ({
37
+ timestamp: new Date(row.publish_time.value),
38
+ attributes: row.attributes ? JSON.parse(row.attributes) : {},
39
+ data: JSON.parse(row.data.toString('utf8')),
40
+ }));
41
+ }
@@ -0,0 +1,10 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ import { EventTopicQueryEvents, type QueriedEvent } from '@causa/workspace-core';
3
+ /**
4
+ * Implements {@link EventTopicQueryEvents} for Pub/Sub topics whose events are stored in BigQuery via a BigQuery
5
+ * subscription. The events are read from the dataset configured at `google.pubSub.bigQueryStorage.rawEventsDatasetId`.
6
+ */
7
+ export declare class EventTopicQueryEventsForBigQuery extends EventTopicQueryEvents {
8
+ _call(context: WorkspaceContext): Promise<QueriedEvent[]>;
9
+ _supports(context: WorkspaceContext): boolean;
10
+ }
@@ -0,0 +1,17 @@
1
+ import { callDeferred, WorkspaceContext } from '@causa/workspace';
2
+ import { EventTopicQueryEvents, } from '@causa/workspace-core';
3
+ /**
4
+ * Implements {@link EventTopicQueryEvents} for Pub/Sub topics whose events are stored in BigQuery via a BigQuery
5
+ * subscription. The events are read from the dataset configured at `google.pubSub.bigQueryStorage.rawEventsDatasetId`.
6
+ */
7
+ export class EventTopicQueryEventsForBigQuery extends EventTopicQueryEvents {
8
+ async _call(context) {
9
+ return await callDeferred(this, context, import.meta.url);
10
+ }
11
+ _supports(context) {
12
+ const conf = context.asConfiguration();
13
+ return (conf.get('events.broker') === 'google.pubSub' &&
14
+ conf.get('events.format') === 'json' &&
15
+ !!conf.get('google.pubSub.bigQueryStorage.rawEventsDatasetId'));
16
+ }
17
+ }
@@ -1 +1,2 @@
1
+ export { DatabaseQueryRecordsForFirestore } from './query-records.js';
1
2
  export { GoogleFirestoreMergeRules } from './merge-rules.js';
@@ -1 +1,2 @@
1
+ export { DatabaseQueryRecordsForFirestore } from './query-records.js';
1
2
  export { GoogleFirestoreMergeRules } from './merge-rules.js';
@@ -0,0 +1,3 @@
1
+ import type { WorkspaceContext } from '@causa/workspace';
2
+ import type { DatabaseQueryRecordsForFirestore } from './query-records.js';
3
+ export default function call(this: DatabaseQueryRecordsForFirestore, context: WorkspaceContext): Promise<any[]>;
@@ -0,0 +1,50 @@
1
+ import { deleteApp, initializeApp } from 'firebase-admin/app';
2
+ import { DocumentReference, GeoPoint, getFirestore, Timestamp, } from 'firebase-admin/firestore';
3
+ import { randomUUID } from 'node:crypto';
4
+ export default async function call(context) {
5
+ if (!this.query) {
6
+ throw new Error(`The 'query' input is required for the 'google.firestore' engine.`);
7
+ }
8
+ const googleConf = context.asConfiguration();
9
+ const projectId = googleConf.getOrThrow('google.project');
10
+ const database = this.database ?? '(default)';
11
+ context.logger.debug(`🗃️ Fetching Firestore document '${this.query}' from database '${database}' in project '${projectId}'.`);
12
+ const app = initializeApp({ projectId }, randomUUID());
13
+ try {
14
+ const firestore = getFirestore(app, database);
15
+ const snapshot = await firestore.doc(this.query).get();
16
+ return snapshot.exists ? [toJson(snapshot.data())] : [];
17
+ }
18
+ finally {
19
+ await deleteApp(app);
20
+ }
21
+ }
22
+ /**
23
+ * Recursively converts Firestore-specific types to JSON-friendly equivalents:
24
+ * - {@link Timestamp} → {@link Date}
25
+ * - {@link GeoPoint} → `{ latitude, longitude }`
26
+ * - {@link DocumentReference} → its `path`
27
+ *
28
+ * Other values (including `Buffer`) are returned as-is.
29
+ */
30
+ function toJson(value) {
31
+ if (value === null || typeof value !== 'object') {
32
+ return value;
33
+ }
34
+ if (value instanceof Timestamp) {
35
+ return value.toDate();
36
+ }
37
+ if (value instanceof GeoPoint) {
38
+ return { latitude: value.latitude, longitude: value.longitude };
39
+ }
40
+ if (value instanceof DocumentReference) {
41
+ return value.path;
42
+ }
43
+ if (Buffer.isBuffer(value)) {
44
+ return value;
45
+ }
46
+ if (Array.isArray(value)) {
47
+ return value.map(toJson);
48
+ }
49
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, toJson(v)]));
50
+ }
@@ -0,0 +1,18 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ import { DatabaseQueryRecords } from '@causa/workspace-core';
3
+ /**
4
+ * The engine identifier handled by this implementation.
5
+ */
6
+ export declare const FIRESTORE_ENGINE = "google.firestore";
7
+ /**
8
+ * Implements {@link DatabaseQueryRecords} for Firestore.
9
+ *
10
+ * The `database` input is optional and defaults to the default Firestore database. The `query` input is the path to a
11
+ * single document (e.g. `users/abc`).
12
+ *
13
+ * Returns an array containing a single document's data, or an empty array if the document does not exist.
14
+ */
15
+ export declare class DatabaseQueryRecordsForFirestore extends DatabaseQueryRecords {
16
+ _call(context: WorkspaceContext): Promise<any[]>;
17
+ _supports(): boolean;
18
+ }
@@ -0,0 +1,22 @@
1
+ import { callDeferred, WorkspaceContext } from '@causa/workspace';
2
+ import { DatabaseQueryRecords } from '@causa/workspace-core';
3
+ /**
4
+ * The engine identifier handled by this implementation.
5
+ */
6
+ export const FIRESTORE_ENGINE = 'google.firestore';
7
+ /**
8
+ * Implements {@link DatabaseQueryRecords} for Firestore.
9
+ *
10
+ * The `database` input is optional and defaults to the default Firestore database. The `query` input is the path to a
11
+ * single document (e.g. `users/abc`).
12
+ *
13
+ * Returns an array containing a single document's data, or an empty array if the document does not exist.
14
+ */
15
+ export class DatabaseQueryRecordsForFirestore extends DatabaseQueryRecords {
16
+ async _call(context) {
17
+ return await callDeferred(this, context, import.meta.url);
18
+ }
19
+ _supports() {
20
+ return this.engine === FIRESTORE_ENGINE;
21
+ }
22
+ }
@@ -1,2 +1,3 @@
1
1
  export { GoogleSpannerListDatabases } from './list-databases.js';
2
+ export { DatabaseQueryRecordsForSpanner } from './query-records.js';
2
3
  export { GoogleSpannerWriteDatabases } from './write-databases.js';
@@ -1,2 +1,3 @@
1
1
  export { GoogleSpannerListDatabases } from './list-databases.js';
2
+ export { DatabaseQueryRecordsForSpanner } from './query-records.js';
2
3
  export { GoogleSpannerWriteDatabases } from './write-databases.js';
@@ -0,0 +1,3 @@
1
+ import type { WorkspaceContext } from '@causa/workspace';
2
+ import { type DatabaseQueryRecordsForSpanner } from './query-records.js';
3
+ export default function call(this: DatabaseQueryRecordsForSpanner, context: WorkspaceContext): Promise<any[]>;