@causa/workspace-google 0.14.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,7 +55,10 @@ 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
 
@@ -51,8 +51,8 @@ export declare enum GoogleCloudRunIngress {
51
51
  * See https://cloud.google.com/run/docs/configuring/connecting-vpc#manage for more details.
52
52
  */
53
53
  export declare enum GoogleCloudRunVpcAccessConnectorEgressSettings {
54
- AllTraffic = "all-traffic",
55
- PrivateRangesOnly = "private-ranges-only"
54
+ AllTraffic = "ALL_TRAFFIC",
55
+ PrivateRangesOnly = "PRIVATE_RANGES_ONLY"
56
56
  }
57
57
  /**
58
58
  * Configuration for Cloud Run containers.
@@ -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.
@@ -597,9 +603,20 @@ export declare class Causa {
597
603
  readonly secrets?: Secrets;
598
604
  [property: string]: any;
599
605
  }
600
- export declare class Secret {
601
- constructor(init: Secret);
606
+ /**
607
+ * A secret that returns a Google access token for the current user or service account.
608
+ * This backend does not require any additional configuration.
609
+ */
610
+ export declare class GoogleSecretConfiguration {
611
+ constructor(init: GoogleSecretConfiguration);
602
612
  readonly backend?: string;
613
+ /**
614
+ * The reference to the secret in Google Secret Manager.
615
+ * Supported formats: `<secret-name>`, `projects/<project>/secrets/<secret-name>`, `projects/<project>/secrets/<secret-name>/versions/<version>`.
616
+ * If the project is not specified, `google.secretManager.project` or `google.project` will be used.
617
+ * If the version is not specified, `latest` will be used.
618
+ */
619
+ readonly id?: string;
603
620
  [property: string]: any;
604
621
  }
605
622
  /**
@@ -609,7 +626,7 @@ export declare class GoogleSecretsConfiguration {
609
626
  constructor(init: GoogleSecretsConfiguration);
610
627
  readonly causa?: Causa;
611
628
  readonly secrets?: {
612
- [key: string]: Secret;
629
+ [key: string]: GoogleSecretConfiguration;
613
630
  };
614
631
  [property: string]: any;
615
632
  }
@@ -92,8 +92,8 @@ export var GoogleCloudRunIngress;
92
92
  */
93
93
  export var GoogleCloudRunVpcAccessConnectorEgressSettings;
94
94
  (function (GoogleCloudRunVpcAccessConnectorEgressSettings) {
95
- GoogleCloudRunVpcAccessConnectorEgressSettings["AllTraffic"] = "all-traffic";
96
- GoogleCloudRunVpcAccessConnectorEgressSettings["PrivateRangesOnly"] = "private-ranges-only";
95
+ GoogleCloudRunVpcAccessConnectorEgressSettings["AllTraffic"] = "ALL_TRAFFIC";
96
+ GoogleCloudRunVpcAccessConnectorEgressSettings["PrivateRangesOnly"] = "PRIVATE_RANGES_ONLY";
97
97
  })(GoogleCloudRunVpcAccessConnectorEgressSettings || (GoogleCloudRunVpcAccessConnectorEgressSettings = {}));
98
98
  /**
99
99
  * Configuration for Cloud Run containers.
@@ -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']),
@@ -193,7 +204,7 @@ __decorate([
193
204
  ], GoogleCloudRun.prototype, "vpcAccessConnector", void 0);
194
205
  __decorate([
195
206
  AllowMissing(),
196
- IsIn(['all-traffic', 'private-ranges-only']),
207
+ IsIn(['ALL_TRAFFIC', 'PRIVATE_RANGES_ONLY']),
197
208
  __metadata("design:type", String)
198
209
  ], GoogleCloudRun.prototype, "vpcAccessConnectorEgressSettings", void 0);
199
210
  /**
@@ -1073,17 +1084,33 @@ __decorate([
1073
1084
  AllowMissing(),
1074
1085
  __metadata("design:type", Secrets)
1075
1086
  ], Causa.prototype, "secrets", void 0);
1076
- export class Secret {
1087
+ /**
1088
+ * A secret that returns a Google access token for the current user or service account.
1089
+ * This backend does not require any additional configuration.
1090
+ */
1091
+ export class GoogleSecretConfiguration {
1077
1092
  constructor(init) {
1078
1093
  Object.assign(this, init);
1079
1094
  }
1080
1095
  backend;
1096
+ /**
1097
+ * The reference to the secret in Google Secret Manager.
1098
+ * Supported formats: `<secret-name>`, `projects/<project>/secrets/<secret-name>`, `projects/<project>/secrets/<secret-name>/versions/<version>`.
1099
+ * If the project is not specified, `google.secretManager.project` or `google.project` will be used.
1100
+ * If the version is not specified, `latest` will be used.
1101
+ */
1102
+ id;
1081
1103
  }
1082
1104
  __decorate([
1083
1105
  AllowMissing(),
1084
1106
  IsString(),
1085
1107
  __metadata("design:type", String)
1086
- ], Secret.prototype, "backend", void 0);
1108
+ ], GoogleSecretConfiguration.prototype, "backend", void 0);
1109
+ __decorate([
1110
+ AllowMissing(),
1111
+ IsString(),
1112
+ __metadata("design:type", String)
1113
+ ], GoogleSecretConfiguration.prototype, "id", void 0);
1087
1114
  /**
1088
1115
  * Google-specific refinement of the secrets configuration.
1089
1116
  */
@@ -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: >-
@@ -404,8 +414,8 @@ properties:
404
414
  title: GoogleCloudRunVpcAccessConnectorEgressSettings
405
415
  type: string
406
416
  enum:
407
- - all-traffic
408
- - private-ranges-only
417
+ - ALL_TRAFFIC
418
+ - PRIVATE_RANGES_ONLY
409
419
  description: >-
410
420
  The setting for egress traffic that goes through the VPC access
411
421
  connector.
@@ -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,4 @@ 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';
@@ -6,3 +6,4 @@ 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';
@@ -1,6 +1,6 @@
1
1
  import { CausaListConfigurationSchemasForGoogle } from './causa/index.js';
2
2
  import { EmulatorStartForFirebaseStorage, EmulatorStartForFirestore, EmulatorStartForIdentityPlatform, EmulatorStartForPubSub, EmulatorStartForSpanner, EmulatorStopForFirebaseStorage, EmulatorStopForFirestore, EmulatorStopForIdentityPlatform, EmulatorStopForPubSub, EmulatorStopForSpanner, } from './emulator/index.js';
3
- import { EventTopicBrokerCreateTopicForPubSub, EventTopicBrokerCreateTriggerForCloudRun, EventTopicBrokerDeleteTopicForPubSub, EventTopicBrokerDeleteTriggerResourceForCloudRunInvokerRole, EventTopicBrokerDeleteTriggerResourceForPubSubSubscription, EventTopicBrokerDeleteTriggerResourceForServiceAccount, EventTopicBrokerGetTopicIdForPubSub, EventTopicBrokerPublishEventsForGoogle, } from './event-topic/index.js';
3
+ import { EventTopicBrokerCreateTopicForPubSub, EventTopicBrokerCreateTriggerForCloudRun, EventTopicBrokerDeleteTopicForPubSub, EventTopicBrokerDeleteTriggerResourceForCloudRunInvokerRole, EventTopicBrokerDeleteTriggerResourceForPubSubSubscription, EventTopicBrokerDeleteTriggerResourceForServiceAccount, EventTopicBrokerGetTopicIdForPubSub, EventTopicBrokerPublishEventsForGoogle, EventTopicCreateBackfillSourceForBigQuery, } from './event-topic/index.js';
4
4
  import { GoogleAppCheckGenerateToken } from './google-app-check/index.js';
5
5
  import { GoogleFirebaseStorageMergeRules } from './google-firebase-storage/index.js';
6
6
  import { GoogleFirestoreMergeRules } from './google-firestore/index.js';
@@ -12,5 +12,5 @@ import { ProjectGetArtefactDestinationForCloudFunctions, ProjectGetArtefactDesti
12
12
  import { SecretFetchForGoogleAccessToken, SecretFetchForGoogleSecretManager, } from './secret/index.js';
13
13
  import { TypeScriptGetDecoratorRendererForGoogleFirestore, TypeScriptGetDecoratorRendererForGoogleSpanner, } from './typescript/index.js';
14
14
  export function registerFunctions(context) {
15
- context.registerFunctionImplementations(CausaListConfigurationSchemasForGoogle, EmulatorStartForFirebaseStorage, EmulatorStartForFirestore, EmulatorStartForIdentityPlatform, EmulatorStartForPubSub, EmulatorStartForSpanner, EmulatorStopForFirebaseStorage, EmulatorStopForFirestore, EmulatorStopForIdentityPlatform, EmulatorStopForPubSub, EmulatorStopForSpanner, EventTopicBrokerCreateTopicForPubSub, EventTopicBrokerCreateTriggerForCloudRun, EventTopicBrokerDeleteTopicForPubSub, EventTopicBrokerDeleteTriggerResourceForCloudRunInvokerRole, EventTopicBrokerDeleteTriggerResourceForPubSubSubscription, EventTopicBrokerDeleteTriggerResourceForServiceAccount, EventTopicBrokerGetTopicIdForPubSub, EventTopicBrokerPublishEventsForGoogle, GoogleAppCheckGenerateToken, GoogleFirebaseStorageMergeRules, GoogleFirestoreMergeRules, GoogleIdentityPlatformGenerateCustomToken, GoogleIdentityPlatformGenerateToken, GooglePubSubWriteTopics, GoogleServicesEnable, GoogleSpannerListDatabases, GoogleSpannerWriteDatabases, ProjectGetArtefactDestinationForCloudFunctions, ProjectGetArtefactDestinationForCloudRun, ProjectPushArtefactForCloudFunctions, SecretFetchForGoogleAccessToken, SecretFetchForGoogleSecretManager, TypeScriptGetDecoratorRendererForGoogleFirestore, TypeScriptGetDecoratorRendererForGoogleSpanner);
15
+ context.registerFunctionImplementations(CausaListConfigurationSchemasForGoogle, EmulatorStartForFirebaseStorage, EmulatorStartForFirestore, EmulatorStartForIdentityPlatform, EmulatorStartForPubSub, EmulatorStartForSpanner, EmulatorStopForFirebaseStorage, EmulatorStopForFirestore, EmulatorStopForIdentityPlatform, EmulatorStopForPubSub, EmulatorStopForSpanner, EventTopicBrokerCreateTopicForPubSub, EventTopicBrokerCreateTriggerForCloudRun, EventTopicBrokerDeleteTopicForPubSub, EventTopicBrokerDeleteTriggerResourceForCloudRunInvokerRole, EventTopicBrokerDeleteTriggerResourceForPubSubSubscription, EventTopicBrokerDeleteTriggerResourceForServiceAccount, EventTopicBrokerGetTopicIdForPubSub, EventTopicBrokerPublishEventsForGoogle, EventTopicCreateBackfillSourceForBigQuery, GoogleAppCheckGenerateToken, GoogleFirebaseStorageMergeRules, GoogleFirestoreMergeRules, GoogleIdentityPlatformGenerateCustomToken, GoogleIdentityPlatformGenerateToken, GooglePubSubWriteTopics, GoogleServicesEnable, GoogleSpannerListDatabases, GoogleSpannerWriteDatabases, ProjectGetArtefactDestinationForCloudFunctions, ProjectGetArtefactDestinationForCloudRun, ProjectPushArtefactForCloudFunctions, SecretFetchForGoogleAccessToken, SecretFetchForGoogleSecretManager, TypeScriptGetDecoratorRendererForGoogleFirestore, TypeScriptGetDecoratorRendererForGoogleSpanner);
16
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@causa/workspace-google",
3
- "version": "0.14.1",
3
+ "version": "0.16.0",
4
4
  "description": "The Causa workspace module providing many functionalities related to GCP and its services.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,7 +9,7 @@
9
9
  "license": "ISC",
10
10
  "type": "module",
11
11
  "engines": {
12
- "node": ">=20"
12
+ "node": ">=22"
13
13
  },
14
14
  "main": "dist/index.js",
15
15
  "types": "dist/index.d.ts",
@@ -35,10 +35,10 @@
35
35
  "generateConfigurationSchemas": "cs model genCode && perl -pi -e 's|\\@causa/runtime|\\@causa/workspace/validation|g' src/configurations/generated.ts"
36
36
  },
37
37
  "dependencies": {
38
- "@causa/cli": ">= 0.6.6 < 1.0.0",
39
- "@causa/workspace": ">= 0.22.2 < 1.0.0",
40
- "@causa/workspace-core": ">= 0.28.1 < 1.0.0",
41
- "@causa/workspace-typescript": ">= 0.19.0 < 1.0.0",
38
+ "@causa/cli": ">= 0.7.0 < 1.0.0",
39
+ "@causa/workspace": ">= 0.24.0 < 1.0.0",
40
+ "@causa/workspace-core": ">= 0.32.0 < 1.0.0",
41
+ "@causa/workspace-typescript": ">= 0.22.0 < 1.0.0",
42
42
  "@google-cloud/apikeys": "^2.2.1",
43
43
  "@google-cloud/bigquery": "^8.1.1",
44
44
  "@google-cloud/iam-credentials": "^4.2.1",
@@ -50,10 +50,10 @@
50
50
  "@google-cloud/spanner": "^8.6.0",
51
51
  "@google-cloud/storage": "^7.19.0",
52
52
  "class-validator": "^0.15.1",
53
- "firebase": "^12.10.0",
54
- "firebase-admin": "^13.7.0",
55
- "globby": "^16.1.1",
56
- "google-auth-library": "^10.6.1",
53
+ "firebase": "^12.12.1",
54
+ "firebase-admin": "^13.8.0",
55
+ "globby": "^16.2.0",
56
+ "google-auth-library": "^10.6.2",
57
57
  "google-gax": "5.0.6",
58
58
  "googleapis": "^171.4.0",
59
59
  "micromatch": "^4.0.8",
@@ -61,20 +61,20 @@
61
61
  "quicktype-core": "^23.2.6"
62
62
  },
63
63
  "devDependencies": {
64
- "@swc/core": "^1.15.18",
64
+ "@swc/core": "^1.15.30",
65
65
  "@swc/jest": "^0.2.39",
66
- "@tsconfig/node20": "^20.1.9",
66
+ "@tsconfig/node22": "^22.0.5",
67
67
  "@types/jest": "^30.0.0",
68
68
  "@types/micromatch": "^4.0.10",
69
- "@types/node": "^20.19.37",
69
+ "@types/node": "^20.19.39",
70
70
  "copyfiles": "^2.4.1",
71
- "eslint": "^10.0.3",
71
+ "eslint": "^10.2.1",
72
72
  "eslint-config-prettier": "^10.1.8",
73
73
  "eslint-plugin-prettier": "^5.5.5",
74
74
  "jest": "^30.3.0",
75
75
  "jest-extended": "^7.0.0",
76
76
  "rimraf": "^6.1.3",
77
- "typescript": "^5.9.3",
78
- "typescript-eslint": "^8.57.0"
77
+ "typescript": "^6.0.3",
78
+ "typescript-eslint": "^8.59.0"
79
79
  }
80
80
  }
@@ -1,53 +0,0 @@
1
- import { WorkspaceContext } from '@causa/workspace';
2
- import type { BackfillEvent, BackfillEventsSource } from '@causa/workspace-core/backfill';
3
- /**
4
- * An events source that reads events from a BigQuery table.
5
- */
6
- export declare class BigQueryEventsSource implements BackfillEventsSource {
7
- readonly tableId: string;
8
- readonly filter?: string | undefined;
9
- /**
10
- * The BigQuery client.
11
- */
12
- private readonly bigQuery;
13
- /**
14
- * The job from which the events are read.
15
- */
16
- private job;
17
- /**
18
- * The request for the next page of results.
19
- * If this is `null` after initializing the job, there are no more results.
20
- */
21
- private nextPageRequest;
22
- /**
23
- * The logger.
24
- */
25
- private readonly logger;
26
- /**
27
- * Creates a new events source that reads events from a BigQuery table.
28
- *
29
- * @param context The {@link WorkspaceContext}.
30
- * @param tableId The ID of the BigQuery table from which the events should be read.
31
- * @param filter A SQL boolean expression that filters the events from the table.
32
- */
33
- constructor(context: WorkspaceContext, tableId: string, filter?: string | undefined);
34
- /**
35
- * Creates a BigQuery job that reads the events from the table.
36
- * The `data` and `attributes` columns are read to get the corresponding event properties.
37
- * If a filter was specified, it is applied to the query as a where clause.
38
- *
39
- * @returns The BigQuery job that reads the events from the table.
40
- */
41
- private createJob;
42
- getBatch(): Promise<BackfillEvent[] | null>;
43
- dispose(): Promise<void>;
44
- /**
45
- * Tries to create a new {@link BigQueryEventsSource} from the given source string.
46
- *
47
- * @param context The {@link WorkspaceContext}.
48
- * @param source The string representing the source, in the format `bq://<projectId>.<datasetId>.<tableId>`.
49
- * @param filter A SQL boolean expression that filters the events from the table.
50
- * @returns A new {@link BigQueryEventsSource} if the source string is valid, otherwise `null`.
51
- */
52
- static fromSourceAndFilter(context: WorkspaceContext, source: string, filter?: string): Promise<BigQueryEventsSource | null>;
53
- }
@@ -1,101 +0,0 @@
1
- import { WorkspaceContext } from '@causa/workspace';
2
- import { BigQuery, Job } from '@google-cloud/bigquery';
3
- import { BigQueryService } from '../services/index.js';
4
- /**
5
- * The number of rows (events) in a single batch.
6
- */
7
- const BIGQUERY_MAX_RESULTS = 10000;
8
- /**
9
- * An events source that reads events from a BigQuery table.
10
- */
11
- export class BigQueryEventsSource {
12
- tableId;
13
- filter;
14
- /**
15
- * The BigQuery client.
16
- */
17
- bigQuery;
18
- /**
19
- * The job from which the events are read.
20
- */
21
- job = null;
22
- /**
23
- * The request for the next page of results.
24
- * If this is `null` after initializing the job, there are no more results.
25
- */
26
- nextPageRequest = null;
27
- /**
28
- * The logger.
29
- */
30
- logger;
31
- /**
32
- * Creates a new events source that reads events from a BigQuery table.
33
- *
34
- * @param context The {@link WorkspaceContext}.
35
- * @param tableId The ID of the BigQuery table from which the events should be read.
36
- * @param filter A SQL boolean expression that filters the events from the table.
37
- */
38
- constructor(context, tableId, filter) {
39
- this.tableId = tableId;
40
- this.filter = filter;
41
- this.bigQuery = context.service(BigQueryService).bigQuery;
42
- this.logger = context.logger;
43
- }
44
- /**
45
- * Creates a BigQuery job that reads the events from the table.
46
- * The `data` and `attributes` columns are read to get the corresponding event properties.
47
- * If a filter was specified, it is applied to the query as a where clause.
48
- *
49
- * @returns The BigQuery job that reads the events from the table.
50
- */
51
- async createJob() {
52
- let query = `
53
- SELECT
54
- data,
55
- attributes,
56
- FROM
57
- \`${this.tableId}\``;
58
- if (this.filter) {
59
- query += `
60
- WHERE
61
- (${this.filter})`;
62
- }
63
- this.logger.debug(`Creating BigQuery job from query '${query}'.`);
64
- const [job] = await this.bigQuery.createQueryJob(query);
65
- return job;
66
- }
67
- async getBatch() {
68
- if (!this.job) {
69
- this.job = await this.createJob();
70
- }
71
- else if (!this.nextPageRequest) {
72
- return null;
73
- }
74
- const [rows, nextPageRequest] = await this.job.getQueryResults(this.nextPageRequest ?? {
75
- autoPaginate: false,
76
- maxResults: BIGQUERY_MAX_RESULTS,
77
- });
78
- this.nextPageRequest = nextPageRequest ?? null;
79
- return rows.map((row) => ({
80
- data: Buffer.from(row.data),
81
- attributes: JSON.parse(row.attributes),
82
- }));
83
- }
84
- async dispose() { }
85
- /**
86
- * Tries to create a new {@link BigQueryEventsSource} from the given source string.
87
- *
88
- * @param context The {@link WorkspaceContext}.
89
- * @param source The string representing the source, in the format `bq://<projectId>.<datasetId>.<tableId>`.
90
- * @param filter A SQL boolean expression that filters the events from the table.
91
- * @returns A new {@link BigQueryEventsSource} if the source string is valid, otherwise `null`.
92
- */
93
- static async fromSourceAndFilter(context, source, filter) {
94
- const match = source.match(/^bq:\/\/(?<tableId>.+)$/);
95
- if (!match?.groups?.tableId) {
96
- return null;
97
- }
98
- const tableId = match.groups.tableId;
99
- return new BigQueryEventsSource(context, tableId, filter);
100
- }
101
- }
@@ -1,2 +0,0 @@
1
- export { BigQueryEventsSource } from './bigquery.js';
2
- export { PubSubBackfillEventPublisher } from './pubsub.js';
@@ -1,2 +0,0 @@
1
- export { BigQueryEventsSource } from './bigquery.js';
2
- export { PubSubBackfillEventPublisher } from './pubsub.js';
@@ -1,34 +0,0 @@
1
- import { WorkspaceContext } from '@causa/workspace';
2
- import { type BackfillEvent, BackfillEventPublisher } from '@causa/workspace-core/backfill';
3
- /**
4
- * A publisher that publishes events to backfill to a Pub/Sub topic.
5
- */
6
- export declare class PubSubBackfillEventPublisher extends BackfillEventPublisher {
7
- readonly topicId: string;
8
- /**
9
- * The actual publisher, which handles batching and flow control.
10
- */
11
- private readonly publisher;
12
- /**
13
- * Creates a new {@link PubSubBackfillEventPublisher}.
14
- *
15
- * @param context The {@link WorkspaceContext}.
16
- * @param topicId The ID of the Pub/Sub topic to publish to.
17
- * This should be a full Pub/Sub topic ID, e.g. `projects/<projectId>/topics/<topicId>`.
18
- */
19
- constructor(context: WorkspaceContext, topicId: string);
20
- /**
21
- * Publishes an event to the Pub/Sub topic.
22
- * Publishing actually occurs asynchronously. {@link PubSubBackfillEventPublisher.flush} must be called to wait for
23
- * all events to be published.
24
- *
25
- * @param event The event to publish.
26
- * @returns A promise that resolves when the publisher has caught up with publishing, or `null` if other events can be
27
- * published immediately.
28
- */
29
- protected publishEvent(event: BackfillEvent): Promise<void> | null;
30
- /**
31
- * Waits for all events to be published.
32
- */
33
- protected flush(): Promise<void>;
34
- }
@@ -1,65 +0,0 @@
1
- import { WorkspaceContext } from '@causa/workspace';
2
- import { BackfillEventPublisher, } from '@causa/workspace-core/backfill';
3
- import { FlowControlledPublisher } from '@google-cloud/pubsub/build/src/publisher/flow-publisher.js';
4
- import { PubSubService } from '../services/index.js';
5
- /**
6
- * Options when publishing events in batches to Pub/Sub.
7
- */
8
- const PUBLISH_OPTIONS = {
9
- flowControlOptions: {
10
- maxOutstandingBytes: 10 * 1024 * 1024,
11
- maxOutstandingMessages: 1000,
12
- },
13
- batching: {
14
- maxBytes: 10 * 1024 * 1024,
15
- maxMessages: 1000,
16
- maxMilliseconds: 1000,
17
- },
18
- };
19
- /**
20
- * A publisher that publishes events to backfill to a Pub/Sub topic.
21
- */
22
- export class PubSubBackfillEventPublisher extends BackfillEventPublisher {
23
- topicId;
24
- /**
25
- * The actual publisher, which handles batching and flow control.
26
- */
27
- publisher;
28
- /**
29
- * Creates a new {@link PubSubBackfillEventPublisher}.
30
- *
31
- * @param context The {@link WorkspaceContext}.
32
- * @param topicId The ID of the Pub/Sub topic to publish to.
33
- * This should be a full Pub/Sub topic ID, e.g. `projects/<projectId>/topics/<topicId>`.
34
- */
35
- constructor(context, topicId) {
36
- super(context);
37
- this.topicId = topicId;
38
- this.publisher = context
39
- .service(PubSubService)
40
- .pubSub.topic(topicId, PUBLISH_OPTIONS)
41
- .flowControlled();
42
- }
43
- /**
44
- * Publishes an event to the Pub/Sub topic.
45
- * Publishing actually occurs asynchronously. {@link PubSubBackfillEventPublisher.flush} must be called to wait for
46
- * all events to be published.
47
- *
48
- * @param event The event to publish.
49
- * @returns A promise that resolves when the publisher has caught up with publishing, or `null` if other events can be
50
- * published immediately.
51
- */
52
- publishEvent(event) {
53
- return this.publisher.publish({
54
- data: event.data,
55
- attributes: event.attributes,
56
- orderingKey: event.key,
57
- });
58
- }
59
- /**
60
- * Waits for all events to be published.
61
- */
62
- async flush() {
63
- await this.publisher.all();
64
- }
65
- }