@causa/workspace-google 0.1.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 (35) hide show
  1. package/LICENSE.md +15 -0
  2. package/README.md +1 -0
  3. package/dist/cli/google-firebase-storage.d.ts +5 -0
  4. package/dist/cli/google-firebase-storage.js +9 -0
  5. package/dist/cli/google-firestore.d.ts +5 -0
  6. package/dist/cli/google-firestore.js +9 -0
  7. package/dist/cli/google.d.ts +5 -0
  8. package/dist/cli/google.js +7 -0
  9. package/dist/cli/index.d.ts +3 -0
  10. package/dist/cli/index.js +3 -0
  11. package/dist/configurations/google.d.ts +190 -0
  12. package/dist/configurations/google.js +1 -0
  13. package/dist/configurations/index.d.ts +1 -0
  14. package/dist/configurations/index.js +1 -0
  15. package/dist/firebase/index.d.ts +1 -0
  16. package/dist/firebase/index.js +1 -0
  17. package/dist/firebase/rules.d.ts +11 -0
  18. package/dist/firebase/rules.js +55 -0
  19. package/dist/functions/google-firebase-storage-merge-rules.d.ts +31 -0
  20. package/dist/functions/google-firebase-storage-merge-rules.js +58 -0
  21. package/dist/functions/google-firestore-merge-rules.d.ts +31 -0
  22. package/dist/functions/google-firestore-merge-rules.js +58 -0
  23. package/dist/functions/google-services-enable.d.ts +27 -0
  24. package/dist/functions/google-services-enable.js +75 -0
  25. package/dist/functions/index.d.ts +2 -0
  26. package/dist/functions/index.js +7 -0
  27. package/dist/functions/secret-fetch-secret-manager.d.ts +33 -0
  28. package/dist/functions/secret-fetch-secret-manager.js +68 -0
  29. package/dist/index.d.ts +5 -0
  30. package/dist/index.js +7 -0
  31. package/dist/services/index.d.ts +1 -0
  32. package/dist/services/index.js +1 -0
  33. package/dist/services/secret-manager.d.ts +17 -0
  34. package/dist/services/secret-manager.js +21 -0
  35. package/package.json +52 -0
package/LICENSE.md ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2023 Causa
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # `@causa/workspace-google` module
@@ -0,0 +1,5 @@
1
+ import { ParentCliCommandDefinition } from '@causa/cli';
2
+ /**
3
+ * The parent `google firebaseStorage` command.
4
+ */
5
+ export declare const firebaseStorageCommandDefinition: ParentCliCommandDefinition;
@@ -0,0 +1,9 @@
1
+ import { googleCommandDefinition } from './google.js';
2
+ /**
3
+ * The parent `google firebaseStorage` command.
4
+ */
5
+ export const firebaseStorageCommandDefinition = {
6
+ parent: googleCommandDefinition,
7
+ name: 'firebaseStorage',
8
+ description: 'Performs Firebase Storage-related operations.',
9
+ };
@@ -0,0 +1,5 @@
1
+ import { ParentCliCommandDefinition } from '@causa/cli';
2
+ /**
3
+ * The parent `google firestore` command.
4
+ */
5
+ export declare const firestoreCommandDefinition: ParentCliCommandDefinition;
@@ -0,0 +1,9 @@
1
+ import { googleCommandDefinition } from './google.js';
2
+ /**
3
+ * The parent `google firestore` command.
4
+ */
5
+ export const firestoreCommandDefinition = {
6
+ parent: googleCommandDefinition,
7
+ name: 'firestore',
8
+ description: 'Performs Firestore database-related operations.',
9
+ };
@@ -0,0 +1,5 @@
1
+ import { ParentCliCommandDefinition } from '@causa/cli';
2
+ /**
3
+ * The parent `google` command.
4
+ */
5
+ export declare const googleCommandDefinition: ParentCliCommandDefinition;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The parent `google` command.
3
+ */
4
+ export const googleCommandDefinition = {
5
+ name: 'google',
6
+ description: 'Performs GCP-specific operations.',
7
+ };
@@ -0,0 +1,3 @@
1
+ export { firebaseStorageCommandDefinition } from './google-firebase-storage.js';
2
+ export { firestoreCommandDefinition } from './google-firestore.js';
3
+ export { googleCommandDefinition } from './google.js';
@@ -0,0 +1,3 @@
1
+ export { firebaseStorageCommandDefinition } from './google-firebase-storage.js';
2
+ export { firestoreCommandDefinition } from './google-firestore.js';
3
+ export { googleCommandDefinition } from './google.js';
@@ -0,0 +1,190 @@
1
+ /**
2
+ * The schema for Google / GCP configuration.
3
+ */
4
+ export type GoogleConfiguration = {
5
+ /**
6
+ * Configuration for everything Google / GCP-related.
7
+ */
8
+ google?: {
9
+ /**
10
+ * The ID of the default GCP project used when performing operations.
11
+ */
12
+ project?: string;
13
+ /**
14
+ * The ID of a local GCP project (which does not exist on GCP).
15
+ * This is for example used by emulators. It should usually start with `demo-` to be compatible with some emulators.
16
+ */
17
+ localProject?: string;
18
+ /**
19
+ * The list of GCP services on which the project depends.
20
+ * This is useful to automatically enable those services in the GCP project.
21
+ */
22
+ services?: string[];
23
+ /**
24
+ * Configuration for the `gcloud` command.
25
+ * This applies to Dockerized calls to `gcloud` using the official `gcloud` Docker image.
26
+ */
27
+ gcloud?: {
28
+ /**
29
+ * The version of the `gcloud` command.
30
+ */
31
+ version?: string;
32
+ };
33
+ /**
34
+ * Configuration for the Firebase project.
35
+ */
36
+ firebase?: {
37
+ /**
38
+ * An existing service account that can be used to sign Identity Platform custom token.
39
+ * Generating custom tokens by signing them using end-user credentials does not work with Identity Platform.
40
+ * A good choice is usually the service account automatically created by Firebase:
41
+ * `firebase-adminsdk-<random ID>@<GCP project>.iam.gserviceaccount.com`
42
+ * If this is not set and is needed by an operation, an attempt will be made to find this service account in the
43
+ * current `google.project`.
44
+ */
45
+ adminServiceAccount?: string;
46
+ /**
47
+ * The Firebase API key used by clients. Used to perform requests to Firebase as an end user.
48
+ * This can be found in the GCP or Firebase Console.
49
+ * If this is not set and is needed by an operation, an attempt will be made to find the key automatically using
50
+ * the API.
51
+ */
52
+ apiKey?: string;
53
+ /**
54
+ * The domain name for the project.
55
+ * If not set, this default to `<GCP project>.firebaseapp.com`.
56
+ */
57
+ authDomain?: string;
58
+ };
59
+ /**
60
+ * Configuration for Firebase tools (the CLI).
61
+ * This applies to Dockerized calls to the `firebase` CLI.
62
+ */
63
+ firebaseTools?: {
64
+ /**
65
+ * The version of the CLI to use.
66
+ */
67
+ version?: string;
68
+ };
69
+ /**
70
+ * Configuration for the Secret Manager service.
71
+ */
72
+ secretManager?: {
73
+ /**
74
+ * The ID of the default GCP project referenced when fetching secrets.
75
+ */
76
+ project?: string;
77
+ };
78
+ /**
79
+ * Configuration for the Firebase Storage service.
80
+ */
81
+ firebaseStorage?: {
82
+ /**
83
+ * Configuration for the emulator.
84
+ */
85
+ emulator?: {
86
+ /**
87
+ * The name of the local Docker container running the emulator.
88
+ */
89
+ containerName?: string;
90
+ };
91
+ /**
92
+ * A list of glob patterns to find files in the workspace defining Firebase Storage security rules.
93
+ */
94
+ securityRuleFiles?: string[];
95
+ /**
96
+ * The file path, relative to the workspace root, where the merged security rules file is written.
97
+ */
98
+ securityRuleFile?: string;
99
+ };
100
+ /**
101
+ * Configuration for the Identity Platform (formerly Firebase Auth) service.
102
+ */
103
+ identityPlatform?: {
104
+ /**
105
+ * Configuration for the emulator.
106
+ */
107
+ emulator?: {
108
+ /**
109
+ * The name of the local Docker container running the emulator.
110
+ */
111
+ containerName?: string;
112
+ };
113
+ };
114
+ /**
115
+ * Configuration for the Firestore service.
116
+ */
117
+ firestore?: {
118
+ /**
119
+ * Configuration for the emulator.
120
+ */
121
+ emulator?: {
122
+ /**
123
+ * The name of the local Docker container running the emulator.
124
+ */
125
+ containerName?: string;
126
+ };
127
+ /**
128
+ * A list of glob patterns to find files in the workspace defining Firestore security rules.
129
+ */
130
+ securityRuleFiles?: string[];
131
+ /**
132
+ * The file path, relative to the workspace root, where the merged security rules file is written.
133
+ */
134
+ securityRuleFile?: string;
135
+ };
136
+ /**
137
+ * Configuration for the Pub/Sub service.
138
+ */
139
+ pubSub?: {
140
+ /**
141
+ * Configuration for the emulator.
142
+ */
143
+ emulator?: {
144
+ /**
145
+ * The name of the local Docker container running the emulator.
146
+ */
147
+ containerName?: string;
148
+ };
149
+ };
150
+ /**
151
+ * Configuration for the Spanner service.
152
+ */
153
+ spanner?: {
154
+ /**
155
+ * Configuration for the emulator.
156
+ */
157
+ emulator?: {
158
+ /**
159
+ * The name of the local Docker container running the emulator.
160
+ */
161
+ containerName?: string;
162
+ /**
163
+ * The version of the Spanner emulator Docker image to use.
164
+ */
165
+ version?: string;
166
+ /**
167
+ * The name of the Spanner instance that will automatically be created in the emulator.
168
+ */
169
+ instanceName?: string;
170
+ };
171
+ /**
172
+ * Defines how DDLs are found for the Spanner databases in the workspace.
173
+ */
174
+ ddls?: {
175
+ /**
176
+ * The format string using groups from the regular expression used to make the database names.
177
+ */
178
+ format?: string;
179
+ /**
180
+ * A list of glob patterns used to find SQL files containing DDL statements for the Spanner databases.
181
+ */
182
+ globs?: string[];
183
+ /**
184
+ * The regular expression used to extract groups from the SQL file paths.
185
+ */
186
+ regularExpression?: string;
187
+ };
188
+ };
189
+ };
190
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export { GoogleConfiguration } from './google.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export { mergeFirebaseRulesFiles } from './rules.js';
@@ -0,0 +1 @@
1
+ export { mergeFirebaseRulesFiles } from './rules.js';
@@ -0,0 +1,11 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ /**
3
+ * Merges all the Firebase security rules files in the repository into a single file.
4
+ *
5
+ * @param displayableType The type for Firebase security rules, which can be displayed in log messages.
6
+ * @param globs The list of patterns used to find rule files from the root of the workspace.
7
+ * @param template The template that takes the merged rules and wraps them to produce the final rules definition.
8
+ * @param rulesFilePath The destination path where the rules will be written, relative to the workspace root.
9
+ * @param context The {@link WorkspaceContext} to use.
10
+ */
11
+ export declare function mergeFirebaseRulesFiles(displayableType: string, globs: string[], template: (rules: string) => string, rulesFilePath: string, context: WorkspaceContext): Promise<string>;
@@ -0,0 +1,55 @@
1
+ import { mkdir, readFile, writeFile } from 'fs/promises';
2
+ import { globby } from 'globby';
3
+ import { dirname, join } from 'path';
4
+ /**
5
+ * The indent for Firebase security rules once merged in the global file.
6
+ */
7
+ const DOCUMENTS_RULES_INDENT = ' ';
8
+ /**
9
+ * Merges the given input Firebase security rules files into a single rules definition.
10
+ *
11
+ * @param rootPath The absolute path from which `ruleFiles` should be resolved.
12
+ * @param rulesFiles The list of (relative) file paths to the rules.
13
+ * @param template The template function producing the final rules file.
14
+ * @returns The merged rules.
15
+ */
16
+ async function mergeRules(rootPath, rulesFiles, template) {
17
+ const rules = await Promise.all(rulesFiles.map(async (filePath) => {
18
+ const path = join(rootPath, filePath);
19
+ const content = await readFile(path);
20
+ const rules = content
21
+ .toString()
22
+ .replaceAll('\n', `\n${DOCUMENTS_RULES_INDENT}`);
23
+ return `${DOCUMENTS_RULES_INDENT}// ${filePath}\n${DOCUMENTS_RULES_INDENT}${rules}`;
24
+ }));
25
+ const mergedDocumentsRules = rules
26
+ .join('\n')
27
+ .replaceAll(/\s+\n/g, '\n\n')
28
+ .trimEnd();
29
+ return template(mergedDocumentsRules);
30
+ }
31
+ /**
32
+ * Merges all the Firebase security rules files in the repository into a single file.
33
+ *
34
+ * @param displayableType The type for Firebase security rules, which can be displayed in log messages.
35
+ * @param globs The list of patterns used to find rule files from the root of the workspace.
36
+ * @param template The template that takes the merged rules and wraps them to produce the final rules definition.
37
+ * @param rulesFilePath The destination path where the rules will be written, relative to the workspace root.
38
+ * @param context The {@link WorkspaceContext} to use.
39
+ */
40
+ export async function mergeFirebaseRulesFiles(displayableType, globs, template, rulesFilePath, context) {
41
+ context.logger.info(`🛂 Looking for ${displayableType} rules files.`);
42
+ const rootPath = context.rootPath;
43
+ rulesFilePath = join(rootPath, rulesFilePath);
44
+ const files = await globby(globs, { cwd: rootPath, gitignore: true });
45
+ files.sort();
46
+ context.logger.debug(`🛂 Found the following rules files: ${files
47
+ .map((f) => `'${f}'`)
48
+ .join(', ')}.`);
49
+ context.logger.info(`🛂 Merging ${files.length} ${displayableType} rules files.`);
50
+ const mergedRules = await mergeRules(rootPath, files, template);
51
+ context.logger.debug(`🛂 Writing merged ${displayableType} rules to '${rulesFilePath}'.`);
52
+ await mkdir(dirname(rulesFilePath), { recursive: true });
53
+ await writeFile(rulesFilePath, mergedRules);
54
+ return rulesFilePath;
55
+ }
@@ -0,0 +1,31 @@
1
+ import { WorkspaceContext, WorkspaceFunction } from '@causa/workspace';
2
+ import { InfrastructureProcessor } from '@causa/workspace-core';
3
+ import { GoogleConfiguration } from '../configurations/index.js';
4
+ /**
5
+ * The return value of {@link GoogleFirebaseStorageMergeRules}.
6
+ */
7
+ type GoogleFirebaseStorageMergeRulesResult = {
8
+ /**
9
+ * A configuration with `google.firebaseStorage.securityRuleFile` set to the path to the merged rules file.
10
+ */
11
+ configuration: GoogleConfiguration;
12
+ /**
13
+ * The path to the merged rules file.
14
+ */
15
+ securityRuleFile: string;
16
+ };
17
+ /**
18
+ * A function that merges all the Firebase Storage security rules found in the workspace into a single file, which can
19
+ * be used for the emulator and as the configuration of the Firebase Storage service.
20
+ * The `google.firebaseStorage.securityRuleFiles` configuration can be used to pass a list of glob patterns to find
21
+ * security rule files in the workspace.
22
+ * The `google.firebaseStorage.securityRuleFile` configuration can be used to specify the output location of the merged
23
+ * rules file.
24
+ * Returns a configuration with `google.firebaseStorage.securityRuleFile` set, such that the function can be used as a
25
+ * processor.
26
+ */
27
+ export declare class GoogleFirebaseStorageMergeRules extends WorkspaceFunction<Promise<GoogleFirebaseStorageMergeRulesResult>> implements InfrastructureProcessor {
28
+ _call(context: WorkspaceContext): Promise<GoogleFirebaseStorageMergeRulesResult>;
29
+ _supports(): boolean;
30
+ }
31
+ export {};
@@ -0,0 +1,58 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { CliCommand } from '@causa/cli';
8
+ import { WorkspaceFunction } from '@causa/workspace';
9
+ import { firebaseStorageCommandDefinition } from '../cli/index.js';
10
+ import { mergeFirebaseRulesFiles } from '../firebase/index.js';
11
+ /**
12
+ * The default location where the security rules file for Firebase Storage will be written.
13
+ */
14
+ const DEFAULT_FIREBASE_STORAGE_SECURITY_RULE_FILE = 'storage.rules';
15
+ /**
16
+ * A function that merges all the Firebase Storage security rules found in the workspace into a single file, which can
17
+ * be used for the emulator and as the configuration of the Firebase Storage service.
18
+ * The `google.firebaseStorage.securityRuleFiles` configuration can be used to pass a list of glob patterns to find
19
+ * security rule files in the workspace.
20
+ * The `google.firebaseStorage.securityRuleFile` configuration can be used to specify the output location of the merged
21
+ * rules file.
22
+ * Returns a configuration with `google.firebaseStorage.securityRuleFile` set, such that the function can be used as a
23
+ * processor.
24
+ */
25
+ let GoogleFirebaseStorageMergeRules = class GoogleFirebaseStorageMergeRules extends WorkspaceFunction {
26
+ async _call(context) {
27
+ const googleConf = context.asConfiguration();
28
+ let securityRuleFile = googleConf.get('google.firebaseStorage.securityRuleFile') ??
29
+ DEFAULT_FIREBASE_STORAGE_SECURITY_RULE_FILE;
30
+ const globs = googleConf.get('google.firebaseStorage.securityRuleFiles') ?? [];
31
+ securityRuleFile = await mergeFirebaseRulesFiles('Firebase Storage', globs, (rules) => `rules_version = '2';
32
+
33
+ service firebase.storage {
34
+ match /b/{bucket}/o {
35
+ ${rules}
36
+ }
37
+ }
38
+ `, securityRuleFile, context);
39
+ return {
40
+ configuration: { google: { firebaseStorage: { securityRuleFile } } },
41
+ securityRuleFile,
42
+ };
43
+ }
44
+ _supports() {
45
+ return true;
46
+ }
47
+ };
48
+ GoogleFirebaseStorageMergeRules = __decorate([
49
+ CliCommand({
50
+ parent: firebaseStorageCommandDefinition,
51
+ name: 'mergeRules',
52
+ description: `Merge Firebase Storage security rules into a single file.
53
+ Input files are looked for in the workspace using the globs defined in google.firebaseStorage.securityRulesFiles.`,
54
+ summary: 'Merge Firebase Storage security rules into a single file.',
55
+ outputFn: ({ securityRuleFile }) => console.log(securityRuleFile),
56
+ })
57
+ ], GoogleFirebaseStorageMergeRules);
58
+ export { GoogleFirebaseStorageMergeRules };
@@ -0,0 +1,31 @@
1
+ import { WorkspaceContext, WorkspaceFunction } from '@causa/workspace';
2
+ import { InfrastructureProcessor } from '@causa/workspace-core';
3
+ import { GoogleConfiguration } from '../configurations/index.js';
4
+ /**
5
+ * The return value of {@link GoogleFirestoreMergeRules}.
6
+ */
7
+ type GoogleFirestoreMergeRulesResult = {
8
+ /**
9
+ * A configuration with `google.firestore.securityRuleFile` set to the path to the merged rules file.
10
+ */
11
+ configuration: GoogleConfiguration;
12
+ /**
13
+ * The path to the merged rules file.
14
+ */
15
+ securityRuleFile: string;
16
+ };
17
+ /**
18
+ * A function that merges all the Firestore security rules found in the workspace into a single file, which can be used
19
+ * for the emulator and as the configuration of the Firestore service.
20
+ * The `google.firestore.securityRuleFiles` configuration can be used to pass a list of glob patterns to find security
21
+ * rule files in the workspace.
22
+ * The `google.firestore.securityRuleFile` configuration can be used to specify the output location of the merged rules
23
+ * file.
24
+ * Returns a configuration with `google.firestore.securityRuleFile` set, such that the function can be used as a
25
+ * processor.
26
+ */
27
+ export declare class GoogleFirestoreMergeRules extends WorkspaceFunction<Promise<GoogleFirestoreMergeRulesResult>> implements InfrastructureProcessor {
28
+ _call(context: WorkspaceContext): Promise<GoogleFirestoreMergeRulesResult>;
29
+ _supports(): boolean;
30
+ }
31
+ export {};
@@ -0,0 +1,58 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { CliCommand } from '@causa/cli';
8
+ import { WorkspaceFunction } from '@causa/workspace';
9
+ import { firestoreCommandDefinition } from '../cli/index.js';
10
+ import { mergeFirebaseRulesFiles } from '../firebase/index.js';
11
+ /**
12
+ * The default location where the security rules file for Firestore will be written.
13
+ */
14
+ const DEFAULT_FIRESTORE_SECURITY_RULE_FILE = 'firestore.rules';
15
+ /**
16
+ * A function that merges all the Firestore security rules found in the workspace into a single file, which can be used
17
+ * for the emulator and as the configuration of the Firestore service.
18
+ * The `google.firestore.securityRuleFiles` configuration can be used to pass a list of glob patterns to find security
19
+ * rule files in the workspace.
20
+ * The `google.firestore.securityRuleFile` configuration can be used to specify the output location of the merged rules
21
+ * file.
22
+ * Returns a configuration with `google.firestore.securityRuleFile` set, such that the function can be used as a
23
+ * processor.
24
+ */
25
+ let GoogleFirestoreMergeRules = class GoogleFirestoreMergeRules extends WorkspaceFunction {
26
+ async _call(context) {
27
+ const googleConf = context.asConfiguration();
28
+ let securityRuleFile = googleConf.get('google.firestore.securityRuleFile') ??
29
+ DEFAULT_FIRESTORE_SECURITY_RULE_FILE;
30
+ const globs = googleConf.get('google.firestore.securityRuleFiles') ?? [];
31
+ securityRuleFile = await mergeFirebaseRulesFiles('Firestore', globs, (rules) => `rules_version = '2';
32
+
33
+ service cloud.firestore {
34
+ match /databases/{database}/documents {
35
+ ${rules}
36
+ }
37
+ }
38
+ `, securityRuleFile, context);
39
+ return {
40
+ configuration: { google: { firestore: { securityRuleFile } } },
41
+ securityRuleFile,
42
+ };
43
+ }
44
+ _supports() {
45
+ return true;
46
+ }
47
+ };
48
+ GoogleFirestoreMergeRules = __decorate([
49
+ CliCommand({
50
+ parent: firestoreCommandDefinition,
51
+ name: 'mergeRules',
52
+ description: `Merge Firestore security rules into a single file.
53
+ Input files are looked for in the workspace using the globs defined in google.firestore.securityRulesFiles.`,
54
+ summary: 'Merge Firestore security rules into a single file.',
55
+ outputFn: ({ securityRuleFile }) => console.log(securityRuleFile),
56
+ })
57
+ ], GoogleFirestoreMergeRules);
58
+ export { GoogleFirestoreMergeRules };
@@ -0,0 +1,27 @@
1
+ import { WorkspaceContext, WorkspaceFunction } from '@causa/workspace';
2
+ import { InfrastructureProcessor } from '@causa/workspace-core';
3
+ import { GoogleConfiguration } from '../configurations/index.js';
4
+ /**
5
+ * The return value of {@link GoogleServicesEnable}.
6
+ */
7
+ type GoogleServicesEnableResult = {
8
+ /**
9
+ * The configuration that should be merged into the workspace configuration.
10
+ * In case `google.services` was already set, this will be an empty object to avoid duplicates due to the merging
11
+ * strategy.
12
+ */
13
+ configuration: GoogleConfiguration;
14
+ /**
15
+ * The list of services that were enabled.
16
+ */
17
+ services: string[];
18
+ };
19
+ /**
20
+ * Enables GCP services defined in `google.services` for the GCP project defined in `google.project`.
21
+ */
22
+ export declare class GoogleServicesEnable extends WorkspaceFunction<Promise<GoogleServicesEnableResult>> implements InfrastructureProcessor {
23
+ readonly tearDown?: boolean;
24
+ _call(context: WorkspaceContext): Promise<GoogleServicesEnableResult>;
25
+ _supports(): boolean;
26
+ }
27
+ export {};
@@ -0,0 +1,75 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { CliCommand } from '@causa/cli';
11
+ import { WorkspaceFunction } from '@causa/workspace';
12
+ import { AllowMissing } from '@causa/workspace/validation';
13
+ import { ServiceUsageClient } from '@google-cloud/service-usage';
14
+ import { IsBoolean } from 'class-validator';
15
+ import { googleCommandDefinition } from '../cli/index.js';
16
+ /**
17
+ * The maximum number of GCP services that can be enabled at the same time.
18
+ */
19
+ const MAX_SERVICE_BATCH = 20;
20
+ /**
21
+ * Enables GCP services defined in `google.services` for the GCP project defined in `google.project`.
22
+ */
23
+ let GoogleServicesEnable = class GoogleServicesEnable extends WorkspaceFunction {
24
+ tearDown;
25
+ async _call(context) {
26
+ if (this.tearDown) {
27
+ return { configuration: {}, services: [] };
28
+ }
29
+ const googleConf = context.asConfiguration();
30
+ const gcpProject = googleConf.getOrThrow('google.project');
31
+ const services = googleConf.get('google.services');
32
+ if (!services) {
33
+ // Although not very useful, this ensures the services configuration is set after the processor has run.
34
+ return { configuration: { google: { services: [] } }, services: [] };
35
+ }
36
+ // There is little chance the client would be reused, so it is not cached or exposed as a service.
37
+ const serviceUsageClient = new ServiceUsageClient();
38
+ const servicesToEnable = [...services];
39
+ const parent = `projects/${gcpProject}`;
40
+ while (servicesToEnable.length > 0) {
41
+ const serviceIds = servicesToEnable.splice(0, MAX_SERVICE_BATCH);
42
+ context.logger.info(`➕ Enabling GCP service(s) ${serviceIds
43
+ .map((s) => `'${s}'`)
44
+ .join(', ')} in project '${gcpProject}'.`);
45
+ const [operation] = await serviceUsageClient.batchEnableServices({
46
+ parent,
47
+ serviceIds,
48
+ });
49
+ await operation.promise();
50
+ }
51
+ // If `google.services` was already set, returning it would duplicate the list due to the configuration merging
52
+ // strategy (concatenating arrays).
53
+ return { configuration: {}, services };
54
+ }
55
+ _supports() {
56
+ return true;
57
+ }
58
+ };
59
+ __decorate([
60
+ IsBoolean(),
61
+ AllowMissing(),
62
+ __metadata("design:type", Boolean)
63
+ ], GoogleServicesEnable.prototype, "tearDown", void 0);
64
+ GoogleServicesEnable = __decorate([
65
+ CliCommand({
66
+ parent: googleCommandDefinition,
67
+ name: 'enableServices',
68
+ description: `Enables the GCP services used by the current project.
69
+ Required GCP services should be defined in the 'google.services' configuration.
70
+ They will be enabled in the 'google.project' GCP project.`,
71
+ summary: 'Enables the GCP services used by the current project.',
72
+ outputFn: ({ services }) => console.log(services.join('\n')),
73
+ })
74
+ ], GoogleServicesEnable);
75
+ export { GoogleServicesEnable };
@@ -0,0 +1,2 @@
1
+ import { ModuleRegistrationContext } from '@causa/workspace';
2
+ export declare function registerFunctions(context: ModuleRegistrationContext): void;
@@ -0,0 +1,7 @@
1
+ import { GoogleFirebaseStorageMergeRules } from './google-firebase-storage-merge-rules.js';
2
+ import { GoogleFirestoreMergeRules } from './google-firestore-merge-rules.js';
3
+ import { GoogleServicesEnable } from './google-services-enable.js';
4
+ import { SecretFetchForGoogleSecretManager } from './secret-fetch-secret-manager.js';
5
+ export function registerFunctions(context) {
6
+ context.registerFunctionImplementations(GoogleFirebaseStorageMergeRules, GoogleFirestoreMergeRules, GoogleServicesEnable, SecretFetchForGoogleSecretManager);
7
+ }
@@ -0,0 +1,33 @@
1
+ import { SecretFetch, WorkspaceContext } from '@causa/workspace';
2
+ /**
3
+ * An error thrown when the default GCP project is needed to look up a secret but it has not been defined.
4
+ */
5
+ export declare class UndefinedDefaultGcpProjectError extends Error {
6
+ constructor();
7
+ }
8
+ /**
9
+ * An error thrown when the value returned by the Google Secret Manager client cannot be read as a string.
10
+ */
11
+ export declare class UnexpectedSecretValueError extends Error {
12
+ constructor();
13
+ }
14
+ /**
15
+ * Implements {@link SecretFetch} for secrets using the Google Secret Manager backend (`google.secretManager`).
16
+ *
17
+ * ```yaml
18
+ * secrets:
19
+ * simpleSecret:
20
+ * id: simple-secret
21
+ * secretWithProject:
22
+ * id: projects/gcp-project/secrets/my-secret
23
+ * secretWithVersion:
24
+ * id: projects/gcp-project/secrets/my-secret/versions/12
25
+ * ```
26
+ *
27
+ * If not specified, the GCP project ID will be obtained from `google.secretManager.project`, or will fallback to
28
+ * `google.project`.
29
+ */
30
+ export declare class SecretFetchForGoogleSecretManager extends SecretFetch {
31
+ _call(context: WorkspaceContext): Promise<string>;
32
+ _supports(): boolean;
33
+ }
@@ -0,0 +1,68 @@
1
+ import { InvalidSecretDefinitionError, SecretFetch, } from '@causa/workspace';
2
+ import { GoogleSecretManagerService } from '../services/index.js';
3
+ /**
4
+ * The regular expression used to match the (Secret Manager) secret ID/name, and possibly its version and project.
5
+ * [projects/<projectId>/secrets]<secretName>[/versions/<version>]
6
+ */
7
+ const SECRET_ID_REGEX = /^(?:projects\/(?<projectId>[\w-]+)\/secrets\/)?(?<secretName>[\w-]+)(?:\/versions\/(?<version>(\d+|latest)))?$/;
8
+ /**
9
+ * An error thrown when the default GCP project is needed to look up a secret but it has not been defined.
10
+ */
11
+ export class UndefinedDefaultGcpProjectError extends Error {
12
+ constructor() {
13
+ super('The default GCP project for the Google Secret Manager could not be found in the configuration.');
14
+ }
15
+ }
16
+ /**
17
+ * An error thrown when the value returned by the Google Secret Manager client cannot be read as a string.
18
+ */
19
+ export class UnexpectedSecretValueError extends Error {
20
+ constructor() {
21
+ super('The secret value from the Google Secret Manager could not be parsed.');
22
+ }
23
+ }
24
+ /**
25
+ * Implements {@link SecretFetch} for secrets using the Google Secret Manager backend (`google.secretManager`).
26
+ *
27
+ * ```yaml
28
+ * secrets:
29
+ * simpleSecret:
30
+ * id: simple-secret
31
+ * secretWithProject:
32
+ * id: projects/gcp-project/secrets/my-secret
33
+ * secretWithVersion:
34
+ * id: projects/gcp-project/secrets/my-secret/versions/12
35
+ * ```
36
+ *
37
+ * If not specified, the GCP project ID will be obtained from `google.secretManager.project`, or will fallback to
38
+ * `google.project`.
39
+ */
40
+ export class SecretFetchForGoogleSecretManager extends SecretFetch {
41
+ async _call(context) {
42
+ const id = this.configuration.id;
43
+ if (!id) {
44
+ throw new InvalidSecretDefinitionError(`Missing 'id' field that should reference the secret.`);
45
+ }
46
+ const match = id.match(SECRET_ID_REGEX);
47
+ const secretName = match?.groups?.secretName;
48
+ if (!secretName) {
49
+ throw new InvalidSecretDefinitionError(`Failed to parse secret reference '${id}'.`);
50
+ }
51
+ const { client, defaultProject } = context.service(GoogleSecretManagerService);
52
+ const project = match?.groups?.projectId ?? defaultProject;
53
+ const version = match?.groups?.version ?? 'latest';
54
+ if (!project) {
55
+ throw new UndefinedDefaultGcpProjectError();
56
+ }
57
+ const name = client.secretVersionPath(project, secretName, version);
58
+ const [value] = await client.accessSecretVersion({ name });
59
+ const bufferData = value.payload?.data;
60
+ if (!bufferData || typeof bufferData !== 'object') {
61
+ throw new UnexpectedSecretValueError();
62
+ }
63
+ return bufferData.toString();
64
+ }
65
+ _supports() {
66
+ return this.backend === 'google.secretManager';
67
+ }
68
+ }
@@ -0,0 +1,5 @@
1
+ import { ModuleRegistrationFunction } from '@causa/workspace';
2
+ export * from './configurations/index.js';
3
+ export * from './services/index.js';
4
+ declare const registerModule: ModuleRegistrationFunction;
5
+ export default registerModule;
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import { registerFunctions } from './functions/index.js';
2
+ export * from './configurations/index.js';
3
+ export * from './services/index.js';
4
+ const registerModule = async (context) => {
5
+ registerFunctions(context);
6
+ };
7
+ export default registerModule;
@@ -0,0 +1 @@
1
+ export { GoogleSecretManagerService } from './secret-manager.js';
@@ -0,0 +1 @@
1
+ export { GoogleSecretManagerService } from './secret-manager.js';
@@ -0,0 +1,17 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
3
+ /**
4
+ * A service exposing a client to the Google Secret Manager.
5
+ */
6
+ export declare class GoogleSecretManagerService {
7
+ /**
8
+ * The default project to use when fetching secrets.
9
+ * Can be `undefined`, in which case the project should be set in the secret itself.
10
+ */
11
+ readonly defaultProject: string | undefined;
12
+ /**
13
+ * The client to the Google Secret Manager.
14
+ */
15
+ readonly client: SecretManagerServiceClient;
16
+ constructor(context: WorkspaceContext);
17
+ }
@@ -0,0 +1,21 @@
1
+ import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
2
+ /**
3
+ * A service exposing a client to the Google Secret Manager.
4
+ */
5
+ export class GoogleSecretManagerService {
6
+ /**
7
+ * The default project to use when fetching secrets.
8
+ * Can be `undefined`, in which case the project should be set in the secret itself.
9
+ */
10
+ defaultProject;
11
+ /**
12
+ * The client to the Google Secret Manager.
13
+ */
14
+ client;
15
+ constructor(context) {
16
+ this.defaultProject =
17
+ context.get('google.secretManager.project') ??
18
+ context.get('google.project');
19
+ this.client = new SecretManagerServiceClient();
20
+ }
21
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@causa/workspace-google",
3
+ "version": "0.1.0",
4
+ "description": "The Causa workspace module providing many functionalities related to GCP and its services.",
5
+ "repository": "github:causa-io/workspace-module-google",
6
+ "license": "ISC",
7
+ "type": "module",
8
+ "engines": {
9
+ "node": ">=16"
10
+ },
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "exports": {
14
+ ".": "./dist/index.js"
15
+ },
16
+ "files": [
17
+ "dist/",
18
+ "LICENSE.md",
19
+ "package.json",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "prebuild": "rimraf ./dist",
24
+ "build": "tsc -p tsconfig.build.json",
25
+ "format": "prettier --write \"src/**/*.ts\"",
26
+ "lint": "eslint \"src/**/*.ts\"",
27
+ "test": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings=ExperimentalWarning\" jest",
28
+ "test:cov": "npm run test -- --coverage"
29
+ },
30
+ "dependencies": {
31
+ "@causa/cli": ">= 0.2.1 < 1.0.0",
32
+ "@causa/workspace": ">= 0.5.0 < 1.0.0",
33
+ "@causa/workspace-core": ">= 0.2.0 < 1.0.0",
34
+ "@google-cloud/secret-manager": "^4.2.2",
35
+ "@google-cloud/service-usage": "^2.2.2",
36
+ "class-validator": "^0.14.0",
37
+ "globby": "^13.1.4"
38
+ },
39
+ "devDependencies": {
40
+ "@tsconfig/node18": "^2.0.1",
41
+ "@types/jest": "^29.5.1",
42
+ "eslint": "^8.40.0",
43
+ "eslint-config-prettier": "^8.8.0",
44
+ "eslint-plugin-prettier": "^4.2.1",
45
+ "jest": "^29.5.0",
46
+ "jest-extended": "^3.2.4",
47
+ "rimraf": "^5.0.1",
48
+ "ts-jest": "^29.1.0",
49
+ "ts-node": "^10.9.1",
50
+ "typescript": "^5.0.4"
51
+ }
52
+ }