@causa/workspace-google 0.9.3 → 0.10.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
@@ -85,28 +85,72 @@ secrets:
85
85
 
86
86
  ### Code generation
87
87
 
88
- This module implements the `google.spanner` TypeScript decorator renderer, which can be used to add `@SpannerTable` and `@SpannerColumn` decorators to classes generated from events. Below is an example of how to enable it for a JSONSchema object:
88
+ This module provides TypeScript decorator renderers for Spanner and Firestore, which can be used to add `@SpannerTable`, `@SpannerColumn`, `@FirestoreCollection`, and `@SoftDeletedFirestoreCollection` decorators to classes generated from events. Below is an example of how to enable it for a JSONSchema object:
89
89
 
90
90
  ```yaml
91
- title: MyClass
91
+ title: MySpannerTable
92
92
  type: object
93
93
  additionalProperties: false
94
94
  causa:
95
95
  # This must be set for the decorators to be added to both the class and its properties.
96
96
  # The content of the object will be passed as the argument to the `@SpannerTable` decorator.
97
- tsGoogleSpannerTable:
97
+ googleSpannerTable:
98
98
  primaryKey: [id]
99
99
  properties:
100
100
  id:
101
101
  type: string
102
102
  format: uuid
103
- # In most cases, the property-level `tsGoogleSpannerColumn` attribute does not need to be set. The decorator configuration will be automatically inferred.
104
- # If needed, the content of `tsGoogleSpannerColumn` will be passed as the argument to the `@SpannerColumn` decorator.
103
+ # In most cases, the property-level `googleSpannerColumn` attribute does not need to be set. The decorator configuration will be automatically inferred.
104
+ # If needed, the content of `googleSpannerColumn` will be passed as the argument to the `@SpannerColumn` decorator.
105
105
  # causa:
106
- # tsGoogleSpannerColumn:
106
+ # googleSpannerColumn:
107
107
  # isJson: false
108
108
  myProperty:
109
109
  type: string
110
+
111
+ ---
112
+ title: MyFirestoreDocument
113
+ type: object
114
+ additionalProperties: false
115
+ causa:
116
+ # This must be set for the decorators to be added to the class.
117
+ googleFirestoreCollection:
118
+ # Mandatory, the name of the Firestore collection.
119
+ name: myCollection
120
+ # Mandatory, determines how to create the path for a document.
121
+ path: [property: id]
122
+ # This could also contain plain strings, e.g. for `{id}/subCollection/{otherProp}`:
123
+ # path: [property: id, subCollection, property: otherProp]
124
+ # Optional, adds the `@SoftDeletedFirestoreCollection` decorator.
125
+ hasSoftDelete: true
126
+ properties:
127
+ id:
128
+ type: string
129
+ otherProp:
130
+ type: string
131
+ ```
132
+
133
+ To restrict the decorator to some schema files, you can configure the parent `typescriptModelClass` generator:
134
+
135
+ ```yaml
136
+ model:
137
+ codeGenerators:
138
+ - generator: typescriptModelClass
139
+
140
+ # ...Rest of the configuration...
141
+
142
+ google:
143
+ spanner:
144
+ # Decorators will only be added to the schemas in those files, relative to the project directory.
145
+ globs:
146
+ - ../entities/*.yaml
147
+ # The name of the property / column to which the `softDelete` option should be added.
148
+ softDeletionColumn: deletedAt
149
+
150
+ firestore:
151
+ # Same as Spanner globs.
152
+ globs:
153
+ - ../firestore/*.yaml
110
154
  ```
111
155
 
112
156
  ## 🔨 Custom `google` commands
@@ -7,8 +7,8 @@ import { PubSubService } from '../services/index.js';
7
7
  */
8
8
  const PUBLISH_OPTIONS = {
9
9
  flowControlOptions: {
10
- maxOutstandingBytes: 512 * 1024 * 1024,
11
- maxOutstandingMessages: 100000,
10
+ maxOutstandingBytes: 10 * 1024 * 1024,
11
+ maxOutstandingMessages: 1000,
12
12
  },
13
13
  batching: {
14
14
  maxBytes: 10 * 1024 * 1024,
@@ -0,0 +1,12 @@
1
+ import { type ClassContext, type TypeScriptDecorator, TypeScriptWithDecoratorsRenderer } from '@causa/workspace-typescript';
2
+ /**
3
+ * A {@link TypeScriptDecoratorsRenderer} that adds Google Firestore decorators from the Causa Google runtime.
4
+ *
5
+ * If an object schema is marked with the `googleFirestoreCollection` attribute, the `@FirestoreCollection` decorator
6
+ * is added to the class. If the `hasSoftDelete` property is `true`, the `@SoftDeletedFirestoreCollection` decorator
7
+ * is also added.
8
+ */
9
+ export declare class GoogleFirestoreRenderer extends TypeScriptWithDecoratorsRenderer {
10
+ decoratorsForClass(context: ClassContext): TypeScriptDecorator[];
11
+ decoratorsForProperty(): TypeScriptDecorator[];
12
+ }
@@ -0,0 +1,90 @@
1
+ import { TypeScriptWithDecoratorsRenderer, } from '@causa/workspace-typescript';
2
+ import { Name, panic } from 'quicktype-core';
3
+ import { schemaMatchesGlobPatterns } from './utils.js';
4
+ /**
5
+ * The name of the Causa attribute that should be present for a class to be decorated with Google Firestore decorators.
6
+ */
7
+ const GOOGLE_FIRESTORE_COLLECTION_ATTRIBUTE = 'googleFirestoreCollection';
8
+ /**
9
+ * The name of the Causa module for the TypeScript Google runtime.
10
+ */
11
+ const CAUSA_GOOGLE_MODULE = '@causa/runtime-google';
12
+ /**
13
+ * A {@link TypeScriptDecoratorsRenderer} that adds Google Firestore decorators from the Causa Google runtime.
14
+ *
15
+ * If an object schema is marked with the `googleFirestoreCollection` attribute, the `@FirestoreCollection` decorator
16
+ * is added to the class. If the `hasSoftDelete` property is `true`, the `@SoftDeletedFirestoreCollection` decorator
17
+ * is also added.
18
+ */
19
+ export class GoogleFirestoreRenderer extends TypeScriptWithDecoratorsRenderer {
20
+ decoratorsForClass(context) {
21
+ const collectionAttribute = context.objectAttributes[GOOGLE_FIRESTORE_COLLECTION_ATTRIBUTE];
22
+ const globs = this.targetLanguage.options.generatorOptions?.google?.firestore?.globs;
23
+ if (!collectionAttribute ||
24
+ !schemaMatchesGlobPatterns(this, context, globs)) {
25
+ return [];
26
+ }
27
+ const debugName = context.classType.getCombinedName();
28
+ if (typeof collectionAttribute !== 'object') {
29
+ panic(`Invalid '${GOOGLE_FIRESTORE_COLLECTION_ATTRIBUTE}' attribute on '${debugName}'. Expected an object.`);
30
+ }
31
+ const { name, path, hasSoftDelete } = collectionAttribute;
32
+ if (typeof name !== 'string') {
33
+ panic(`Invalid '${GOOGLE_FIRESTORE_COLLECTION_ATTRIBUTE}' attribute on '${debugName}'. Expected an object with a 'name' string property.`);
34
+ }
35
+ if (!Array.isArray(path)) {
36
+ panic(`Invalid '${GOOGLE_FIRESTORE_COLLECTION_ATTRIBUTE}' attribute on '${debugName}'. Expected an object with a 'path' array property.`);
37
+ }
38
+ const requiredProperties = path.flatMap((e) => typeof e === 'object' &&
39
+ e !== null &&
40
+ 'property' in e &&
41
+ typeof e.property === 'string'
42
+ ? [e.property]
43
+ : []);
44
+ const propertyReferences = new Map();
45
+ this.forEachClassProperty(context.classType, 'none', (name, jsonName) => {
46
+ if (requiredProperties.includes(jsonName)) {
47
+ propertyReferences.set(jsonName, name);
48
+ }
49
+ });
50
+ for (const propertyName of requiredProperties) {
51
+ if (!propertyReferences.has(propertyName)) {
52
+ panic(`Property '${propertyName}' referenced in 'path' not found in '${debugName}'.`);
53
+ }
54
+ }
55
+ const elements = path.map((element) => {
56
+ if (typeof element === 'string') {
57
+ return JSON.stringify(element);
58
+ }
59
+ if (typeof element === 'object' &&
60
+ element !== null &&
61
+ 'property' in element) {
62
+ const propertyName = element.property;
63
+ const generatedName = propertyReferences.get(propertyName);
64
+ if (!generatedName) {
65
+ panic(`Property '${propertyName}' not found in property references.`);
66
+ }
67
+ return ['doc.', generatedName];
68
+ }
69
+ panic(`Invalid path element in '${GOOGLE_FIRESTORE_COLLECTION_ATTRIBUTE}' attribute on '${debugName}'.`);
70
+ });
71
+ const pathExpression = path.length === 1
72
+ ? elements
73
+ : ['[', ...elements.flatMap((e) => [e, ', ']), "].join('/')"];
74
+ const decorators = [];
75
+ this.addDecoratorToList(decorators, context, 'FirestoreCollection', CAUSA_GOOGLE_MODULE, [
76
+ '@FirestoreCollection({ name: ',
77
+ JSON.stringify(name),
78
+ ', path: (doc) => ',
79
+ ...pathExpression,
80
+ '})',
81
+ ]);
82
+ if (hasSoftDelete === true) {
83
+ this.addDecoratorToList(decorators, context, 'SoftDeletedFirestoreCollection', CAUSA_GOOGLE_MODULE, ['@SoftDeletedFirestoreCollection()']);
84
+ }
85
+ return decorators;
86
+ }
87
+ decoratorsForProperty() {
88
+ return [];
89
+ }
90
+ }
@@ -1,11 +1,11 @@
1
- import { type ClassContext, type ClassPropertyContext, type TypeScriptDecorator, TypeScriptDecoratorsRenderer } from '@causa/workspace-typescript';
1
+ import { type ClassContext, type ClassPropertyContext, type TypeScriptDecorator, TypeScriptWithDecoratorsRenderer } from '@causa/workspace-typescript';
2
2
  /**
3
3
  * A {@link TypeScriptDecoratorsRenderer} that adds Google Spanner decorators from the Causa Google runtime.
4
4
  *
5
- * If an object schema is marked with the `tsGoogleSpannerTable` attribute, the `@SpannerTable` decorator is added to
6
- * the class, and `@SpannerColumn` decorators are added to all its properties.
5
+ * If an object schema is marked with the `googleSpannerTable` attribute, the `@SpannerTable` decorator is added to the
6
+ * class, and `@SpannerColumn` decorators are added to all its properties.
7
7
  */
8
- export declare class GoogleSpannerRenderer extends TypeScriptDecoratorsRenderer {
8
+ export declare class GoogleSpannerRenderer extends TypeScriptWithDecoratorsRenderer {
9
9
  decoratorsForClass(context: ClassContext): TypeScriptDecorator[];
10
10
  decoratorsForProperty(context: ClassPropertyContext): TypeScriptDecorator[];
11
11
  }
@@ -1,24 +1,15 @@
1
- import { TypeScriptDecoratorsRenderer, getSingleType, typeScriptSourceForObject, } from '@causa/workspace-typescript';
1
+ import { TypeScriptWithDecoratorsRenderer, getSingleType, typeScriptSourceForObject, } from '@causa/workspace-typescript';
2
2
  import { panic } from 'quicktype-core';
3
+ import { schemaMatchesGlobPatterns } from './utils.js';
3
4
  /**
4
5
  * The name of the Causa attribute that should be present for a class to be decorated with Google Spanner decorators.
5
6
  */
6
- const GOOGLE_SPANNER_TABLE_ATTRIBUTE = 'tsGoogleSpannerTable';
7
- /**
8
- * The name of the Causa attribute that can be present on a class to only generate `@SpannerColumn` decorators for its
9
- * properties, but not a `@SpannerTable` decorator for the class itself.
10
- */
11
- const GOOGLE_SPANNER_NESTED_TYPE_ATTRIBUTE = 'tsGoogleSpannerNestedType';
7
+ const GOOGLE_SPANNER_TABLE_ATTRIBUTE = 'googleSpannerTable';
12
8
  /**
13
9
  * The name of the optional Causa attribute that can be present on an object property schema to specify options for the
14
10
  * `@SpannerColumn` decorator.
15
11
  */
16
- const GOOGLE_SPANNER_COLUMN_ATTRIBUTE = 'tsGoogleSpannerColumn';
17
- /**
18
- * The name of the `decoratorOptions` key that can be used to specify the name of the property that should be used as
19
- * the soft deletion column.
20
- */
21
- const GOOGLE_SPANNER_SOFT_DELETION_COLUMN_OPTION = 'googleSpannerSoftDeletionColumn';
12
+ const GOOGLE_SPANNER_COLUMN_ATTRIBUTE = 'googleSpannerColumn';
22
13
  /**
23
14
  * The name of the Causa module for the TypeScript Google runtime.
24
15
  */
@@ -28,8 +19,6 @@ const CAUSA_GOOGLE_MODULE = '@causa/runtime-google';
28
19
  * specifies the type of the column. In this case, the renderer should not infer any type information.
29
20
  */
30
21
  const TYPE_INFO_COLUMN_ATTRIBUTE_NAMES = [
31
- 'nestedType',
32
- 'nullifyNested',
33
22
  'isBigInt',
34
23
  'isInt',
35
24
  'isPreciseDate',
@@ -38,55 +27,73 @@ const TYPE_INFO_COLUMN_ATTRIBUTE_NAMES = [
38
27
  /**
39
28
  * A {@link TypeScriptDecoratorsRenderer} that adds Google Spanner decorators from the Causa Google runtime.
40
29
  *
41
- * If an object schema is marked with the `tsGoogleSpannerTable` attribute, the `@SpannerTable` decorator is added to
42
- * the class, and `@SpannerColumn` decorators are added to all its properties.
30
+ * If an object schema is marked with the `googleSpannerTable` attribute, the `@SpannerTable` decorator is added to the
31
+ * class, and `@SpannerColumn` decorators are added to all its properties.
43
32
  */
44
- export class GoogleSpannerRenderer extends TypeScriptDecoratorsRenderer {
33
+ export class GoogleSpannerRenderer extends TypeScriptWithDecoratorsRenderer {
45
34
  decoratorsForClass(context) {
46
35
  const tableAttribute = context.objectAttributes[GOOGLE_SPANNER_TABLE_ATTRIBUTE];
47
- if (!tableAttribute) {
36
+ const globs = this.targetLanguage.options.generatorOptions?.google?.spanner?.globs;
37
+ if (!tableAttribute || !schemaMatchesGlobPatterns(this, context, globs)) {
48
38
  return [];
49
39
  }
50
40
  if (typeof tableAttribute !== 'object' ||
51
41
  !('primaryKey' in tableAttribute) ||
52
- !Array.isArray(tableAttribute.primaryKey)) {
53
- panic(`Invalid ${GOOGLE_SPANNER_TABLE_ATTRIBUTE} attribute on ${context.classType.getNames()}`);
42
+ !Array.isArray(tableAttribute.primaryKey) ||
43
+ tableAttribute.primaryKey.length === 0 ||
44
+ tableAttribute.primaryKey.some((k) => typeof k !== 'string')) {
45
+ panic(`Invalid '${GOOGLE_SPANNER_TABLE_ATTRIBUTE}' attribute on '${context.classType.getCombinedName()}'. Expected an object with a 'primaryKey' array.`);
46
+ }
47
+ if ('name' in tableAttribute && typeof tableAttribute.name !== 'string') {
48
+ panic(`Invalid 'name' in '${GOOGLE_SPANNER_TABLE_ATTRIBUTE}' attribute on '${context.classType.getCombinedName()}'. Expected a string.`);
54
49
  }
55
- const optionsSource = typeScriptSourceForObject(tableAttribute);
50
+ const optionsSource = typeScriptSourceForObject({
51
+ primaryKey: tableAttribute.primaryKey,
52
+ name: tableAttribute.name,
53
+ });
56
54
  const decorators = [];
57
55
  this.addDecoratorToList(decorators, context, 'SpannerTable', CAUSA_GOOGLE_MODULE, ['@SpannerTable(', optionsSource, ')']);
58
56
  return decorators;
59
57
  }
60
58
  decoratorsForProperty(context) {
61
- if (!context.objectAttributes[GOOGLE_SPANNER_TABLE_ATTRIBUTE] &&
62
- !context.objectAttributes[GOOGLE_SPANNER_NESTED_TYPE_ATTRIBUTE]) {
59
+ const globs = this.targetLanguage.options.generatorOptions?.google?.spanner?.globs;
60
+ if (!context.objectAttributes[GOOGLE_SPANNER_TABLE_ATTRIBUTE] ||
61
+ !schemaMatchesGlobPatterns(this, context, globs)) {
63
62
  return [];
64
63
  }
65
- const rawColumnAttributes = context.propertyAttributes[GOOGLE_SPANNER_COLUMN_ATTRIBUTE] ?? {};
66
- const columnAttributes = typeof rawColumnAttributes === 'object' ? rawColumnAttributes : {};
67
- const softDeletionColumn = this.decoratorOptions[GOOGLE_SPANNER_SOFT_DELETION_COLUMN_OPTION];
68
- if (softDeletionColumn && context.jsonName === softDeletionColumn) {
64
+ const attributes = context.propertyAttributes[GOOGLE_SPANNER_COLUMN_ATTRIBUTE] ?? {};
65
+ const { generatorOptions } = this.targetLanguage.options;
66
+ const softDeletionColumn = generatorOptions?.google?.spanner?.softDeletionColumn;
67
+ const { name: overriddenName, tsOptions } = attributes;
68
+ if (tsOptions && typeof tsOptions !== 'object') {
69
+ panic(`Invalid 'tsOptions' in '${GOOGLE_SPANNER_COLUMN_ATTRIBUTE}' attribute. Expected an object.`);
70
+ }
71
+ const columnAttributes = tsOptions ?? {};
72
+ if (overriddenName) {
73
+ if (typeof overriddenName !== 'string') {
74
+ panic(`Invalid 'name' in '${GOOGLE_SPANNER_COLUMN_ATTRIBUTE}' attribute. Expected a string.`);
75
+ }
76
+ columnAttributes.name = overriddenName;
77
+ }
78
+ const columnName = overriddenName ?? this.names.get(context.name);
79
+ if (columnName === softDeletionColumn) {
69
80
  columnAttributes.softDelete = true;
70
81
  }
71
- if (!context.propertyAttributes.tsType) {
72
- const singleTypeInfo = getSingleType(context.property.type);
73
- const schemaOverridesTypeInfo = TYPE_INFO_COLUMN_ATTRIBUTE_NAMES.some((name) => name in columnAttributes);
74
- if (!schemaOverridesTypeInfo && singleTypeInfo) {
75
- switch (singleTypeInfo.type.kind) {
76
- case 'class':
77
- case 'object':
78
- case 'map':
79
- columnAttributes.isJson = true;
80
- break;
81
- case 'integer':
82
- columnAttributes.isInt = true;
83
- break;
84
- }
82
+ const singleTypeInfo = getSingleType(context.property.type);
83
+ const schemaOverridesTypeInfo = TYPE_INFO_COLUMN_ATTRIBUTE_NAMES.some((name) => name in columnAttributes);
84
+ if (!schemaOverridesTypeInfo && singleTypeInfo) {
85
+ switch (singleTypeInfo.type.kind) {
86
+ case 'class':
87
+ case 'object':
88
+ case 'map':
89
+ columnAttributes.isJson = true;
90
+ break;
91
+ case 'integer':
92
+ columnAttributes.isInt = true;
93
+ break;
85
94
  }
86
95
  }
87
- const optionsSource = typeScriptSourceForObject(columnAttributes, {
88
- encoder: (key, value) => key === 'nestedType' ? value : JSON.stringify(value),
89
- });
96
+ const optionsSource = typeScriptSourceForObject(columnAttributes);
90
97
  const decorators = [];
91
98
  this.addDecoratorToList(decorators, context, 'SpannerColumn', CAUSA_GOOGLE_MODULE, ['@SpannerColumn(', optionsSource, ')']);
92
99
  return decorators;
@@ -1 +1,2 @@
1
+ export { GoogleFirestoreRenderer } from './google-firestore-renderer.js';
1
2
  export { GoogleSpannerRenderer } from './google-spanner-renderer.js';
@@ -1 +1,2 @@
1
+ export { GoogleFirestoreRenderer } from './google-firestore-renderer.js';
1
2
  export { GoogleSpannerRenderer } from './google-spanner-renderer.js';
@@ -0,0 +1,12 @@
1
+ import type { ClassContext } from '@causa/workspace-typescript';
2
+ import { TypeScriptWithDecoratorsRenderer } from '@causa/workspace-typescript';
3
+ /**
4
+ * Checks whether the schema matches the glob patterns specified in the renderer's options.
5
+ * If this returns `false`, the decorators should not be generated for the schema.
6
+ *
7
+ * @param renderer The renderer instance.
8
+ * @param context The {@link ClassContext} of the schema to check.
9
+ * @param globs The glob patterns to match against, or undefined/null if not configured.
10
+ * @returns `true` if the decorators should be generated for the schema.
11
+ */
12
+ export declare function schemaMatchesGlobPatterns(renderer: TypeScriptWithDecoratorsRenderer, context: ClassContext, globs: unknown): boolean;
@@ -0,0 +1,21 @@
1
+ import { TypeScriptWithDecoratorsRenderer } from '@causa/workspace-typescript';
2
+ import micromatch from 'micromatch';
3
+ import { join } from 'path';
4
+ /**
5
+ * Checks whether the schema matches the glob patterns specified in the renderer's options.
6
+ * If this returns `false`, the decorators should not be generated for the schema.
7
+ *
8
+ * @param renderer The renderer instance.
9
+ * @param context The {@link ClassContext} of the schema to check.
10
+ * @param globs The glob patterns to match against, or undefined/null if not configured.
11
+ * @returns `true` if the decorators should be generated for the schema.
12
+ */
13
+ export function schemaMatchesGlobPatterns(renderer, context, globs) {
14
+ const { uri } = context;
15
+ if (!uri || !globs || !Array.isArray(globs)) {
16
+ return true;
17
+ }
18
+ const projectPath = renderer.targetLanguage.workspaceContext.getProjectPathOrThrow();
19
+ const absoluteGlobs = globs.map((g) => join(projectPath, g));
20
+ return micromatch.isMatch(uri, absoluteGlobs);
21
+ }
@@ -9,7 +9,7 @@ import { GoogleServicesEnable } from './google-services/index.js';
9
9
  import { GoogleSpannerListDatabases, GoogleSpannerWriteDatabases, } from './google-spanner/index.js';
10
10
  import { ProjectGetArtefactDestinationForCloudFunctions, ProjectGetArtefactDestinationForCloudRun, ProjectPushArtefactForCloudFunctions, } from './project/index.js';
11
11
  import { SecretFetchForGoogleAccessToken, SecretFetchForGoogleSecretManager, } from './secret/index.js';
12
- import { TypeScriptGetDecoratorRendererForGoogleSpanner } from './typescript/index.js';
12
+ import { TypeScriptGetDecoratorRendererForGoogleFirestore, TypeScriptGetDecoratorRendererForGoogleSpanner, } from './typescript/index.js';
13
13
  export function registerFunctions(context) {
14
- context.registerFunctionImplementations(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, TypeScriptGetDecoratorRendererForGoogleSpanner);
14
+ context.registerFunctionImplementations(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
15
  }
@@ -0,0 +1,9 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ import { TypeScriptGetDecoratorRenderer, TypeScriptWithDecoratorsRenderer } from '@causa/workspace-typescript';
3
+ /**
4
+ * Implements {@link TypeScriptGetDecoratorRenderer} for the {@link GoogleFirestoreRenderer}.
5
+ */
6
+ export declare class TypeScriptGetDecoratorRendererForGoogleFirestore extends TypeScriptGetDecoratorRenderer {
7
+ _call(): new (...args: any[]) => TypeScriptWithDecoratorsRenderer;
8
+ _supports(context: WorkspaceContext): boolean;
9
+ }
@@ -0,0 +1,15 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ import { TypeScriptGetDecoratorRenderer, TypeScriptWithDecoratorsRenderer, } from '@causa/workspace-typescript';
3
+ import { GoogleFirestoreRenderer } from '../../code-generation/index.js';
4
+ /**
5
+ * Implements {@link TypeScriptGetDecoratorRenderer} for the {@link GoogleFirestoreRenderer}.
6
+ */
7
+ export class TypeScriptGetDecoratorRendererForGoogleFirestore extends TypeScriptGetDecoratorRenderer {
8
+ _call() {
9
+ return GoogleFirestoreRenderer;
10
+ }
11
+ _supports(context) {
12
+ return (context.get('project.language') === 'typescript' &&
13
+ this.generator === 'typescriptModelClass');
14
+ }
15
+ }
@@ -1,10 +1,9 @@
1
1
  import { WorkspaceContext } from '@causa/workspace';
2
- import { TypeScriptDecoratorsRenderer, TypeScriptGetDecoratorRenderer } from '@causa/workspace-typescript';
2
+ import { TypeScriptGetDecoratorRenderer, TypeScriptWithDecoratorsRenderer } from '@causa/workspace-typescript';
3
3
  /**
4
4
  * Implements {@link TypeScriptGetDecoratorRenderer} for the {@link GoogleSpannerRenderer}.
5
- * The configuration name for the renderer is `google.spanner`.
6
5
  */
7
6
  export declare class TypeScriptGetDecoratorRendererForGoogleSpanner extends TypeScriptGetDecoratorRenderer {
8
- _call(): new (...args: any[]) => TypeScriptDecoratorsRenderer;
7
+ _call(): new (...args: any[]) => TypeScriptWithDecoratorsRenderer;
9
8
  _supports(context: WorkspaceContext): boolean;
10
9
  }
@@ -1,22 +1,15 @@
1
1
  import { WorkspaceContext } from '@causa/workspace';
2
- import { TypeScriptDecoratorsRenderer, TypeScriptGetDecoratorRenderer, } from '@causa/workspace-typescript';
2
+ import { TypeScriptGetDecoratorRenderer, TypeScriptWithDecoratorsRenderer, } from '@causa/workspace-typescript';
3
3
  import { GoogleSpannerRenderer } from '../../code-generation/index.js';
4
4
  /**
5
5
  * Implements {@link TypeScriptGetDecoratorRenderer} for the {@link GoogleSpannerRenderer}.
6
- * The configuration name for the renderer is `google.spanner`.
7
6
  */
8
7
  export class TypeScriptGetDecoratorRendererForGoogleSpanner extends TypeScriptGetDecoratorRenderer {
9
8
  _call() {
10
9
  return GoogleSpannerRenderer;
11
10
  }
12
11
  _supports(context) {
13
- if (context.get('project.language') !== 'typescript') {
14
- return false;
15
- }
16
- const decoratorRenderers = context
17
- .asConfiguration()
18
- .get('typescript.codeGeneration.decoratorRenderers') ?? [];
19
- return (decoratorRenderers.length === 0 ||
20
- decoratorRenderers.includes('google.spanner'));
12
+ return (context.get('project.language') === 'typescript' &&
13
+ this.generator === 'typescriptModelClass');
21
14
  }
22
15
  }
@@ -1 +1,2 @@
1
+ export { TypeScriptGetDecoratorRendererForGoogleFirestore } from './get-decorator-renderer-google-firestore.js';
1
2
  export { TypeScriptGetDecoratorRendererForGoogleSpanner } from './get-decorator-renderer-google-spanner.js';
@@ -1 +1,2 @@
1
+ export { TypeScriptGetDecoratorRendererForGoogleFirestore } from './get-decorator-renderer-google-firestore.js';
1
2
  export { TypeScriptGetDecoratorRendererForGoogleSpanner } from './get-decorator-renderer-google-spanner.js';
@@ -2,6 +2,7 @@ import { WorkspaceContext } from '@causa/workspace';
2
2
  import { EventTopicTriggerCreationError } from '@causa/workspace-core';
3
3
  import { Subscription } from '@google-cloud/pubsub';
4
4
  import { randomBytes } from 'crypto';
5
+ import { grpc } from 'google-gax';
5
6
  import { CloudRunService } from './cloud-run.js';
6
7
  import { IamService } from './iam.js';
7
8
  import { PubSubService } from './pubsub.js';
@@ -109,7 +110,6 @@ export class CloudRunPubSubTriggerService {
109
110
  }
110
111
  this.invokerBindingIds.add(invokerBindingId);
111
112
  this.logger.info(`🛂 Granting invoker IAM role to backfilling service account '${pubSubServiceAccount}' for Cloud Run service '${serviceId}'.`);
112
- // This may fail due to eventual consistency during service account creation.
113
113
  await this.cloudRunService.addInvokerBinding(serviceId, pubSubServiceAccount);
114
114
  return invokerBindingId;
115
115
  }
@@ -138,6 +138,8 @@ export class CloudRunPubSubTriggerService {
138
138
  minimumBackoff: { seconds: 1, nanos: 0 },
139
139
  maximumBackoff: { seconds: 60, nanos: 0 },
140
140
  },
141
+ // This can occur due to eventual consistency when the service account is created.
142
+ gaxOpts: { retry: { retryCodes: [grpc.status.INVALID_ARGUMENT] } },
141
143
  });
142
144
  return subscriptionId;
143
145
  }
@@ -1,4 +1,5 @@
1
1
  import { ServicesClient } from '@google-cloud/run';
2
+ import { grpc } from 'google-gax';
2
3
  /**
3
4
  * The role used to allow a service account to call a Cloud Run service.
4
5
  */
@@ -39,11 +40,9 @@ export class CloudRunService {
39
40
  const members = [`serviceAccount:${serviceAccountEmail}`];
40
41
  const binding = { role: INVOKER_ROLE, members };
41
42
  policy.bindings = [...(policy.bindings ?? []), binding];
42
- // This may fail due to eventual consistency during service account creation.
43
- await this.servicesClient.setIamPolicy({
44
- resource: serviceId,
45
- policy,
46
- });
43
+ await this.servicesClient.setIamPolicy({ resource: serviceId, policy },
44
+ // This can occur due to eventual consistency when the service account is created.
45
+ { retry: { retryCodes: [grpc.status.INVALID_ARGUMENT] } });
47
46
  }
48
47
  /**
49
48
  * Removes a service account from the list of allowed invokers of a Cloud Run service.
@@ -17,16 +17,12 @@ export class ResourceManagerService {
17
17
  * @returns The number of the GCP project.
18
18
  */
19
19
  async getProjectNumber(projectId) {
20
- const [projects] = await this.projectsClient.searchProjects({
21
- query: `projectId:${projectId}`,
22
- pageSize: 1,
23
- });
20
+ const [projects] = await this.projectsClient.searchProjects({ query: `projectId:${projectId}`, pageSize: 1 }, { autoPaginate: false });
24
21
  if (projects.length < 1) {
25
22
  throw new Error(`Could not find GCP project '${projectId}'.`);
26
23
  }
27
24
  const [project] = projects;
28
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
29
- const [projectsConst, projectNumber] = project.name?.split('/') ?? [];
25
+ const projectNumber = project.name?.split('/').at(1);
30
26
  if (!projectNumber) {
31
27
  throw new Error(`Failed to parse invalid project name '${project.name}'.`);
32
28
  }
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@causa/workspace-google",
3
- "version": "0.9.3",
3
+ "version": "0.10.0",
4
4
  "description": "The Causa workspace module providing many functionalities related to GCP and its services.",
5
- "repository": "github:causa-io/workspace-module-google",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/causa-io/workspace-module-google.git"
8
+ },
6
9
  "license": "ISC",
7
10
  "type": "module",
8
11
  "engines": {
9
- "node": ">=18"
12
+ "node": ">=20"
10
13
  },
11
14
  "main": "dist/index.js",
12
15
  "types": "dist/index.d.ts",
@@ -29,47 +32,49 @@
29
32
  "test:cov": "npm run test -- --coverage"
30
33
  },
31
34
  "dependencies": {
32
- "@causa/cli": ">= 0.6.1 < 1.0.0",
33
- "@causa/workspace": ">= 0.16.1 < 1.0.0",
34
- "@causa/workspace-core": ">= 0.22.3 < 1.0.0",
35
- "@causa/workspace-typescript": ">= 0.10.2 < 1.0.0",
36
- "@google-cloud/apikeys": "^2.1.0",
37
- "@google-cloud/bigquery": "^8.0.0",
38
- "@google-cloud/iam-credentials": "^4.0.1",
39
- "@google-cloud/pubsub": "^5.0.0",
40
- "@google-cloud/resource-manager": "^6.0.1",
41
- "@google-cloud/run": "^2.0.1",
42
- "@google-cloud/secret-manager": "^6.0.1",
43
- "@google-cloud/service-usage": "^4.1.0",
44
- "@google-cloud/spanner": "7.21.0",
35
+ "@causa/cli": ">= 0.6.2 < 1.0.0",
36
+ "@causa/workspace": ">= 0.18.0 < 1.0.0",
37
+ "@causa/workspace-core": ">= 0.23.0 < 1.0.0",
38
+ "@causa/workspace-typescript": ">= 0.11.0 < 1.0.0",
39
+ "@google-cloud/apikeys": "^2.2.0",
40
+ "@google-cloud/bigquery": "^8.1.1",
41
+ "@google-cloud/iam-credentials": "^4.2.0",
42
+ "@google-cloud/pubsub": "^5.1.0",
43
+ "@google-cloud/resource-manager": "^6.2.0",
44
+ "@google-cloud/run": "^2.3.0",
45
+ "@google-cloud/secret-manager": "^6.1.0",
46
+ "@google-cloud/service-usage": "^4.2.0",
47
+ "@google-cloud/spanner": "8.1.0",
45
48
  "@google-cloud/storage": "^7.16.0",
46
49
  "class-validator": "^0.14.2",
47
- "firebase": "^11.7.1",
48
- "firebase-admin": "^13.3.0",
50
+ "firebase": "^12.0.0",
51
+ "firebase-admin": "^13.4.0",
49
52
  "globby": "^14.1.0",
50
- "google-auth-library": "^9.15.1",
51
- "google-gax": "4.4.1",
52
- "googleapis": "^148.0.0",
53
- "pino": "^9.6.0",
54
- "quicktype-core": "^23.1.4",
53
+ "google-auth-library": "^10.2.1",
54
+ "google-gax": "5.0.1-rc.1",
55
+ "googleapis": "^155.0.0",
56
+ "micromatch": "^4.0.8",
57
+ "pino": "^9.7.0",
58
+ "quicktype-core": "^23.2.6",
55
59
  "uuid": "^11.1.0"
56
60
  },
57
61
  "devDependencies": {
58
- "@swc/core": "^1.11.24",
59
- "@swc/jest": "^0.2.38",
60
- "@tsconfig/node20": "^20.1.5",
61
- "@types/jest": "^29.5.14",
62
- "@types/node": "^18.19.100",
62
+ "@swc/core": "^1.13.3",
63
+ "@swc/jest": "^0.2.39",
64
+ "@tsconfig/node20": "^20.1.6",
65
+ "@types/jest": "^30.0.0",
66
+ "@types/micromatch": "^4.0.9",
67
+ "@types/node": "^22.17.0",
63
68
  "@types/uuid": "^10.0.0",
64
69
  "copyfiles": "^2.4.1",
65
- "eslint": "^9.26.0",
66
- "eslint-config-prettier": "^10.1.5",
67
- "eslint-plugin-prettier": "^5.4.0",
68
- "jest": "^29.7.0",
69
- "jest-extended": "^4.0.2",
70
+ "eslint": "^9.32.0",
71
+ "eslint-config-prettier": "^10.1.8",
72
+ "eslint-plugin-prettier": "^5.5.3",
73
+ "jest": "^30.0.5",
74
+ "jest-extended": "^6.0.0",
70
75
  "rimraf": "^6.0.1",
71
76
  "ts-node": "^10.9.2",
72
- "typescript": "^5.8.3",
73
- "typescript-eslint": "^8.32.0"
77
+ "typescript": "^5.9.2",
78
+ "typescript-eslint": "^8.39.0"
74
79
  }
75
80
  }