@causa/workspace-google 1.4.0-beta.2 → 1.4.0-beta.3
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 +5 -3
- package/dist/functions/event-topic/count-events-bigquery.call.d.ts +3 -0
- package/dist/functions/event-topic/count-events-bigquery.call.js +75 -0
- package/dist/functions/event-topic/count-events-bigquery.d.ts +11 -0
- package/dist/functions/event-topic/count-events-bigquery.js +19 -0
- package/dist/functions/event-topic/create-backfill-source-bigquery.call.js +6 -0
- package/dist/functions/event-topic/create-backfill-source-bigquery.d.ts +1 -1
- package/dist/functions/event-topic/create-backfill-source-bigquery.js +1 -1
- package/dist/functions/event-topic/index.d.ts +1 -0
- package/dist/functions/event-topic/index.js +1 -0
- package/dist/functions/event-topic/query-events-bigquery.call.js +2 -0
- package/dist/functions/event-topic/query-events-bigquery.d.ts +1 -0
- package/dist/functions/event-topic/query-events-bigquery.js +1 -0
- package/dist/functions/google-spanner/count-records.call.d.ts +16 -0
- package/dist/functions/google-spanner/count-records.call.js +120 -0
- package/dist/functions/google-spanner/count-records.d.ts +15 -0
- package/dist/functions/google-spanner/count-records.js +21 -0
- package/dist/functions/google-spanner/index.d.ts +3 -1
- package/dist/functions/google-spanner/index.js +3 -1
- package/dist/functions/google-spanner/list-databases.d.ts +22 -1
- package/dist/functions/google-spanner/list-databases.js +27 -15
- package/dist/functions/google-spanner/resolve-table.d.ts +25 -0
- package/dist/functions/google-spanner/resolve-table.js +44 -0
- package/dist/functions/index.js +3 -3
- package/dist/graph/rules/configuration/service-container.js +4 -4
- package/dist/graph/rules/terraform/spanner-databases.js +4 -4
- package/dist/graph/spanner.d.ts +9 -58
- package/dist/graph/spanner.js +24 -68
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ Backfilling is supported when `google.pubSub` is set as the `events.broker`. Tem
|
|
|
60
60
|
- As a raw URI, using the format `[[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger`.
|
|
61
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`.
|
|
62
62
|
|
|
63
|
-
When no source is specified, the default is to fetch events to backfill from the BigQuery dataset configured in `google.pubSub.bigQueryStorage
|
|
63
|
+
When no source is specified, the default is to fetch events to backfill from the BigQuery dataset configured in `google.pubSub.bigQueryStorage`, once per Pub/Sub message ID. 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.
|
|
64
64
|
|
|
65
65
|
The Pub/Sub publish options used when backfilling can be tuned through `google.pubSub.backfillPublishOptions`, which exposes the `maxOutstandingBytes`, `maxOutstandingMessages`, `maxBytes`, `maxMessages`, and `maxMilliseconds` fields.
|
|
66
66
|
|
|
@@ -68,11 +68,13 @@ By default, project-scoped triggers send backfill events to the live Cloud Run s
|
|
|
68
68
|
|
|
69
69
|
### Querying
|
|
70
70
|
|
|
71
|
-
This module implements the `DatabaseQueryRecords`, `ServiceContainerQueryLogs`, and `
|
|
71
|
+
This module implements the `DatabaseQueryRecords`, `DatabaseCountRecords`, `ServiceContainerQueryLogs`, `EventTopicQueryEvents`, and `EventTopicCountEvents` workspace functions, which expose read-only access to data stored in or emitted by deployed services:
|
|
72
72
|
|
|
73
73
|
- `DatabaseQueryRecords` is supported for the `google.spanner` and `google.firestore` engines. For Spanner, the function takes a database name and a SQL query, runs it on the configured Spanner instance, and returns the matching rows (capped to 100000). For Firestore, the query is the path to a single document, fetched from the default database (or the specified one). The document data is returned as a single-element array. An empty array is returned when the document does not exist.
|
|
74
|
+
- `DatabaseCountRecords` is supported for the `google.spanner` engine. It counts the rows of the table in the schema's `googleSpannerTable` binding, in the database whose DDL creates it, or the one in the domain of the schema when several databases create a table with that name. Properties are read from the column named by their `googleSpannerColumn` extension, or from the column with the same name, and nested properties are read from JSON columns, where a value only equals a constant of the same JSON type.
|
|
74
75
|
- `ServiceContainerQueryLogs` is supported when `serviceContainer.platform` is `google.cloudRun`. It queries Cloud Logging for log entries emitted by the service's Cloud Run revisions. By default, only the last hour of logs and up to 1 000 entries are returned.
|
|
75
|
-
- `EventTopicQueryEvents` is supported when `events.broker` is `google.pubSub`, `events.format` is `json`, and `google.pubSub.bigQueryStorage.rawEventsDatasetId` is set. It reads events from the topic's BigQuery raw events table. Same defaults as for log queries: last hour and up to 1 000 events.
|
|
76
|
+
- `EventTopicQueryEvents` is supported when `events.broker` is `google.pubSub`, `events.format` is `json`, and `google.pubSub.bigQueryStorage.rawEventsDatasetId` is set. It reads events from the topic's BigQuery raw events table, once per Pub/Sub message ID, as the BigQuery subscription may write a message several times. Same defaults as for log queries: last hour and up to 1 000 events.
|
|
77
|
+
- `EventTopicCountEvents` is supported in the same cases as `EventTopicQueryEvents`. It counts the events published within the time range in the topic's BigQuery raw events table, whose `data` column should be of the `JSON` type, once per Pub/Sub message ID. A value only equals a constant of the same JSON type.
|
|
76
78
|
|
|
77
79
|
### Architecture graph
|
|
78
80
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { BigQueryService } from '../../services/index.js';
|
|
2
|
+
import { rawEventsTableName } from '../google-pubsub/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Returns the function converting a JSON value to the type of the given value, without failing for other types.
|
|
5
|
+
*
|
|
6
|
+
* @param value The value to which the JSON value is compared.
|
|
7
|
+
* @returns The name of the BigQuery conversion function.
|
|
8
|
+
*/
|
|
9
|
+
function laxConversion(value) {
|
|
10
|
+
switch (typeof value) {
|
|
11
|
+
case 'string':
|
|
12
|
+
return 'LAX_STRING';
|
|
13
|
+
case 'number':
|
|
14
|
+
return 'LAX_FLOAT64';
|
|
15
|
+
case 'boolean':
|
|
16
|
+
return 'LAX_BOOL';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Builds the condition on the event payload, stored in the `data` JSON column, for a {@link RecordCondition}.
|
|
21
|
+
*
|
|
22
|
+
* @param condition The condition.
|
|
23
|
+
* @param addParam A function adding a parameter to the query, and returning its name.
|
|
24
|
+
* @returns The SQL condition.
|
|
25
|
+
*/
|
|
26
|
+
function sqlCondition(condition, addParam) {
|
|
27
|
+
const value = `data${condition.path.map((key) => `[${JSON.stringify(key)}]`).join('')}`;
|
|
28
|
+
switch (condition.operator) {
|
|
29
|
+
case 'isNull':
|
|
30
|
+
return `(${value} IS NULL OR JSON_TYPE(${value}) = 'null')`;
|
|
31
|
+
case 'isNotNull':
|
|
32
|
+
return `JSON_TYPE(${value}) != 'null'`;
|
|
33
|
+
case 'equals':
|
|
34
|
+
// The type is checked as lax conversions also convert values of other types, e.g. the string `"false"` to `FALSE`.
|
|
35
|
+
return `JSON_TYPE(${value}) = '${typeof condition.value}' AND ${laxConversion(condition.value)}(${value}) = @${addParam(condition.value)}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export default async function call() {
|
|
39
|
+
const googleConf = this._context.asConfiguration();
|
|
40
|
+
const projectId = googleConf.getOrThrow('google.project');
|
|
41
|
+
const rawEventsDatasetId = googleConf.getOrThrow('google.pubSub.bigQueryStorage.rawEventsDatasetId');
|
|
42
|
+
const tableId = `${projectId}.${rawEventsDatasetId}.${rawEventsTableName(this.topic)}`;
|
|
43
|
+
const names = Object.keys(this.filters);
|
|
44
|
+
const params = {
|
|
45
|
+
from: this.from.toISOString(),
|
|
46
|
+
to: this.to.toISOString(),
|
|
47
|
+
};
|
|
48
|
+
let paramCount = 0;
|
|
49
|
+
const addParam = (value) => {
|
|
50
|
+
const param = `p${paramCount++}`;
|
|
51
|
+
params[param] = value;
|
|
52
|
+
return param;
|
|
53
|
+
};
|
|
54
|
+
const counts = names.map((name, index) => {
|
|
55
|
+
const conditions = this.filters[name].map((c) => sqlCondition(c, addParam));
|
|
56
|
+
return `COUNTIF(${conditions.join(' AND ') || 'TRUE'}) AS f${index}`;
|
|
57
|
+
});
|
|
58
|
+
// Events written several times to the table by the BigQuery subscription are only counted once.
|
|
59
|
+
const query = `
|
|
60
|
+
WITH events AS (
|
|
61
|
+
SELECT data
|
|
62
|
+
FROM \`${tableId}\`
|
|
63
|
+
WHERE publish_time >= @from AND publish_time < @to
|
|
64
|
+
QUALIFY ROW_NUMBER() OVER (PARTITION BY message_id ORDER BY publish_time) = 1
|
|
65
|
+
)
|
|
66
|
+
SELECT ${['COUNT(*) AS total', ...counts].join(', ')}
|
|
67
|
+
FROM events`;
|
|
68
|
+
this._context.logger.debug(`Counting events in BigQuery:\n${query}`);
|
|
69
|
+
const bigQuery = this._context.service(BigQueryService).bigQuery;
|
|
70
|
+
const [[row]] = await bigQuery.query({ query, params });
|
|
71
|
+
return {
|
|
72
|
+
total: Number(row.total),
|
|
73
|
+
counts: Object.fromEntries(names.map((name, index) => [name, Number(row[`f${index}`])])),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { EventTopicCountEvents, type RecordCounts } from '@causa/workspace-core';
|
|
2
|
+
/**
|
|
3
|
+
* Implements {@link EventTopicCountEvents} for Pub/Sub topics whose events are stored in BigQuery via a BigQuery
|
|
4
|
+
* subscription. The events are read from the dataset configured at `google.pubSub.bigQueryStorage.rawEventsDatasetId`,
|
|
5
|
+
* whose `data` column is expected to be of the `JSON` type.
|
|
6
|
+
* Events are counted once per Pub/Sub message ID, and the time range applies to their publish time.
|
|
7
|
+
*/
|
|
8
|
+
export declare class EventTopicCountEventsForBigQuery extends EventTopicCountEvents {
|
|
9
|
+
_call(): Promise<RecordCounts>;
|
|
10
|
+
_supports(): boolean;
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { callDeferred } from '@causa/workspace';
|
|
2
|
+
import { EventTopicCountEvents, } from '@causa/workspace-core';
|
|
3
|
+
/**
|
|
4
|
+
* Implements {@link EventTopicCountEvents} for Pub/Sub topics whose events are stored in BigQuery via a BigQuery
|
|
5
|
+
* subscription. The events are read from the dataset configured at `google.pubSub.bigQueryStorage.rawEventsDatasetId`,
|
|
6
|
+
* whose `data` column is expected to be of the `JSON` type.
|
|
7
|
+
* Events are counted once per Pub/Sub message ID, and the time range applies to their publish time.
|
|
8
|
+
*/
|
|
9
|
+
export class EventTopicCountEventsForBigQuery extends EventTopicCountEvents {
|
|
10
|
+
async _call() {
|
|
11
|
+
return await callDeferred(this, import.meta.url);
|
|
12
|
+
}
|
|
13
|
+
_supports() {
|
|
14
|
+
const conf = this._context.asConfiguration();
|
|
15
|
+
return (conf.get('events.broker') === 'google.pubSub' &&
|
|
16
|
+
conf.get('events.format') === 'json' &&
|
|
17
|
+
!!conf.get('google.pubSub.bigQueryStorage.rawEventsDatasetId'));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -34,6 +34,12 @@ export default async function call() {
|
|
|
34
34
|
WHERE
|
|
35
35
|
(${this.filter})`;
|
|
36
36
|
}
|
|
37
|
+
if (!this.source) {
|
|
38
|
+
// Events written several times to the default storage by the BigQuery subscription are only backfilled once.
|
|
39
|
+
query += `
|
|
40
|
+
QUALIFY
|
|
41
|
+
ROW_NUMBER() OVER (PARTITION BY message_id ORDER BY publish_time) = 1`;
|
|
42
|
+
}
|
|
37
43
|
this._context.logger.debug(`Creating BigQuery job from query '${query}'.`);
|
|
38
44
|
const bigQuery = this._context.service(BigQueryService).bigQuery;
|
|
39
45
|
const [job] = await bigQuery.createQueryJob(query);
|
|
@@ -9,7 +9,7 @@ export declare const BIGQUERY_SOURCE_REGEX: RegExp;
|
|
|
9
9
|
* Two cases are supported:
|
|
10
10
|
* - The `source` is a `bq://<projectId>.<datasetId>.<tableId>` URI.
|
|
11
11
|
* - The `source` is omitted and the broker is `google.pubSub` with `google.pubSub.bigQueryStorage.rawEventsDatasetId`
|
|
12
|
-
* configured: events are read from the default BigQuery storage for the topic.
|
|
12
|
+
* configured: events are read from the default BigQuery storage for the topic, once per Pub/Sub message ID.
|
|
13
13
|
*
|
|
14
14
|
* The returned async iterable pages through the query results and yields each row as a {@link BackfillEvent}.
|
|
15
15
|
*/
|
|
@@ -10,7 +10,7 @@ export const BIGQUERY_SOURCE_REGEX = /^bq:\/\/(?<tableId>.+)$/;
|
|
|
10
10
|
* Two cases are supported:
|
|
11
11
|
* - The `source` is a `bq://<projectId>.<datasetId>.<tableId>` URI.
|
|
12
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.
|
|
13
|
+
* configured: events are read from the default BigQuery storage for the topic, once per Pub/Sub message ID.
|
|
14
14
|
*
|
|
15
15
|
* The returned async iterable pages through the query results and yields each row as a {@link BackfillEvent}.
|
|
16
16
|
*/
|
|
@@ -8,5 +8,6 @@ export { EventTopicBrokerDeleteTriggerResourceForServiceAccount } from './broker
|
|
|
8
8
|
export { EventTopicBrokerGetTopicIdForPubSub } from './broker-get-topic-id-pubsub.js';
|
|
9
9
|
export { EventTopicBrokerPublishEventsForGoogle } from './broker-publish-events-google.js';
|
|
10
10
|
export { EventTopicBrokerWaitForProcessingForPubSub } from './broker-wait-for-processing-pubsub.js';
|
|
11
|
+
export { EventTopicCountEventsForBigQuery } from './count-events-bigquery.js';
|
|
11
12
|
export { EventTopicCreateBackfillSourceForBigQuery } from './create-backfill-source-bigquery.js';
|
|
12
13
|
export { EventTopicQueryEventsForBigQuery } from './query-events-bigquery.js';
|
|
@@ -8,5 +8,6 @@ export { EventTopicBrokerDeleteTriggerResourceForServiceAccount } from './broker
|
|
|
8
8
|
export { EventTopicBrokerGetTopicIdForPubSub } from './broker-get-topic-id-pubsub.js';
|
|
9
9
|
export { EventTopicBrokerPublishEventsForGoogle } from './broker-publish-events-google.js';
|
|
10
10
|
export { EventTopicBrokerWaitForProcessingForPubSub } from './broker-wait-for-processing-pubsub.js';
|
|
11
|
+
export { EventTopicCountEventsForBigQuery } from './count-events-bigquery.js';
|
|
11
12
|
export { EventTopicCreateBackfillSourceForBigQuery } from './create-backfill-source-bigquery.js';
|
|
12
13
|
export { EventTopicQueryEventsForBigQuery } from './query-events-bigquery.js';
|
|
@@ -24,10 +24,12 @@ export default async function call() {
|
|
|
24
24
|
if (this.filter) {
|
|
25
25
|
conditions.push(`(${this.filter})`);
|
|
26
26
|
}
|
|
27
|
+
// Events written several times to the table by the BigQuery subscription are only returned once.
|
|
27
28
|
const query = `
|
|
28
29
|
SELECT message_id, publish_time, data, attributes
|
|
29
30
|
FROM \`${tableId}\`
|
|
30
31
|
WHERE ${conditions.join(' AND ')}
|
|
32
|
+
QUALIFY ROW_NUMBER() OVER (PARTITION BY message_id ORDER BY publish_time) = 1
|
|
31
33
|
ORDER BY publish_time ASC
|
|
32
34
|
LIMIT ${limit}`;
|
|
33
35
|
this._context.logger.debug(`Querying BigQuery for events:\n${query}`);
|
|
@@ -2,6 +2,7 @@ import { EventTopicQueryEvents, type QueriedEvent } from '@causa/workspace-core'
|
|
|
2
2
|
/**
|
|
3
3
|
* Implements {@link EventTopicQueryEvents} for Pub/Sub topics whose events are stored in BigQuery via a BigQuery
|
|
4
4
|
* subscription. The events are read from the dataset configured at `google.pubSub.bigQueryStorage.rawEventsDatasetId`.
|
|
5
|
+
* Events are returned once per Pub/Sub message ID.
|
|
5
6
|
*/
|
|
6
7
|
export declare class EventTopicQueryEventsForBigQuery extends EventTopicQueryEvents {
|
|
7
8
|
_call(): Promise<QueriedEvent[]>;
|
|
@@ -3,6 +3,7 @@ import { EventTopicQueryEvents, } from '@causa/workspace-core';
|
|
|
3
3
|
/**
|
|
4
4
|
* Implements {@link EventTopicQueryEvents} for Pub/Sub topics whose events are stored in BigQuery via a BigQuery
|
|
5
5
|
* subscription. The events are read from the dataset configured at `google.pubSub.bigQueryStorage.rawEventsDatasetId`.
|
|
6
|
+
* Events are returned once per Pub/Sub message ID.
|
|
6
7
|
*/
|
|
7
8
|
export class EventTopicQueryEventsForBigQuery extends EventTopicQueryEvents {
|
|
8
9
|
async _call() {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ObjectSchema, RecordCondition, RecordCounts } from '@causa/workspace-core';
|
|
2
|
+
import type { DatabaseCountRecordsForSpanner } from './count-records.js';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the SQL query counting the rows of a table, and those matching each filter.
|
|
5
|
+
*
|
|
6
|
+
* @param table The name of the table.
|
|
7
|
+
* @param schema The schema persisted in the table.
|
|
8
|
+
* @param filters The conditions of each filter, in order.
|
|
9
|
+
* @returns The SQL query and its parameters. The counts are returned in the `total` column, and the `f<index>` column
|
|
10
|
+
* for each filter.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildCountQuery(table: string, schema: ObjectSchema, filters: RecordCondition[][]): {
|
|
13
|
+
sql: string;
|
|
14
|
+
params: Record<string, string | number | boolean>;
|
|
15
|
+
};
|
|
16
|
+
export default function call(this: DatabaseCountRecordsForSpanner): Promise<RecordCounts>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { splitSchemaPath } from '@causa/workspace-core/jsonschema';
|
|
2
|
+
import { Spanner } from '@google-cloud/spanner';
|
|
3
|
+
import { GoogleSpannerListDatabases } from './list-databases.js';
|
|
4
|
+
import { SPANNER_ENGINE } from './query-records.js';
|
|
5
|
+
import { resolveSpannerTable } from './resolve-table.js';
|
|
6
|
+
/**
|
|
7
|
+
* Finds the database whose DDL creates a table, using {@link resolveSpannerTable}.
|
|
8
|
+
*
|
|
9
|
+
* @param context The {@link WorkspaceContext}.
|
|
10
|
+
* @param table The name of the table.
|
|
11
|
+
* @param schema The schema persisted in the table.
|
|
12
|
+
* @returns The ID of the database.
|
|
13
|
+
*/
|
|
14
|
+
async function findDatabase(context, table, schema) {
|
|
15
|
+
const [databases, domainPaths] = await Promise.all([
|
|
16
|
+
context.call(GoogleSpannerListDatabases, {}),
|
|
17
|
+
context.listDomainPaths(),
|
|
18
|
+
]);
|
|
19
|
+
const { file } = splitSchemaPath(schema.path);
|
|
20
|
+
const resolution = resolveSpannerTable(databases, table, file, domainPaths);
|
|
21
|
+
switch (resolution.kind) {
|
|
22
|
+
case 'resolved':
|
|
23
|
+
return resolution.database.id;
|
|
24
|
+
case 'missing':
|
|
25
|
+
throw new Error(`No Spanner DDL creates the table '${table}'.`);
|
|
26
|
+
case 'ambiguous':
|
|
27
|
+
throw new Error(`The table '${table}' is created in several Spanner databases: ${resolution.candidates
|
|
28
|
+
.map(({ id }) => `'${id}'`)
|
|
29
|
+
.join(', ')}.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns the SQL expression reading the value at a path in the records of a schema.
|
|
34
|
+
*
|
|
35
|
+
* @param schema The schema persisted in the table.
|
|
36
|
+
* @param path The names of the properties leading to the value.
|
|
37
|
+
* @returns The SQL expression, and the JSON value when it reads a nested value from a JSON column.
|
|
38
|
+
*/
|
|
39
|
+
function valueExpression(schema, path) {
|
|
40
|
+
const [name, ...nested] = path;
|
|
41
|
+
const property = schema.properties.find((p) => p.name === name);
|
|
42
|
+
if (!property) {
|
|
43
|
+
throw new Error(`'${name}' is not a property of '${schema.name}'.`);
|
|
44
|
+
}
|
|
45
|
+
const columnExtension = property.extensions.googleSpannerColumn;
|
|
46
|
+
const column = typeof columnExtension?.name === 'string' ? columnExtension.name : name;
|
|
47
|
+
const expression = `\`${column}\``;
|
|
48
|
+
if (nested.length === 0) {
|
|
49
|
+
return { expression };
|
|
50
|
+
}
|
|
51
|
+
const json = `${expression}${nested.map((key) => `[${JSON.stringify(key)}]`).join('')}`;
|
|
52
|
+
return { expression: `JSON_VALUE(${json})`, json };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Builds the SQL query counting the rows of a table, and those matching each filter.
|
|
56
|
+
*
|
|
57
|
+
* @param table The name of the table.
|
|
58
|
+
* @param schema The schema persisted in the table.
|
|
59
|
+
* @param filters The conditions of each filter, in order.
|
|
60
|
+
* @returns The SQL query and its parameters. The counts are returned in the `total` column, and the `f<index>` column
|
|
61
|
+
* for each filter.
|
|
62
|
+
*/
|
|
63
|
+
export function buildCountQuery(table, schema, filters) {
|
|
64
|
+
const params = {};
|
|
65
|
+
const condition = (c) => {
|
|
66
|
+
const { expression, json } = valueExpression(schema, c.path);
|
|
67
|
+
if (c.operator !== 'equals') {
|
|
68
|
+
return `${expression} ${c.operator === 'isNull' ? 'IS NULL' : 'IS NOT NULL'}`;
|
|
69
|
+
}
|
|
70
|
+
const param = `p${Object.keys(params).length}`;
|
|
71
|
+
if (!json) {
|
|
72
|
+
params[param] = c.value;
|
|
73
|
+
return `${expression} = @${param}`;
|
|
74
|
+
}
|
|
75
|
+
// Nested values are read as strings from JSON columns, and only match values of the same JSON type.
|
|
76
|
+
const type = `JSON_TYPE(${json}) = '${typeof c.value}'`;
|
|
77
|
+
if (typeof c.value === 'number') {
|
|
78
|
+
params[param] = c.value;
|
|
79
|
+
return `${type} AND SAFE_CAST(${expression} AS FLOAT64) = @${param}`;
|
|
80
|
+
}
|
|
81
|
+
params[param] = String(c.value);
|
|
82
|
+
return `${type} AND ${expression} = @${param}`;
|
|
83
|
+
};
|
|
84
|
+
const counts = filters.map((conditions, index) => `COUNTIF(${conditions.map(condition).join(' AND ') || 'TRUE'}) AS f${index}`);
|
|
85
|
+
const sql = `SELECT ${['COUNT(*) AS total', ...counts].join(', ')} FROM \`${table}\``;
|
|
86
|
+
return { sql, params };
|
|
87
|
+
}
|
|
88
|
+
export default async function call() {
|
|
89
|
+
const table = this.schema.databases.find((d) => d.engine === SPANNER_ENGINE)?.table;
|
|
90
|
+
if (!table) {
|
|
91
|
+
throw new Error(`The schema '${this.schema.name}' is not persisted in a Spanner table.`);
|
|
92
|
+
}
|
|
93
|
+
const names = Object.keys(this.filters);
|
|
94
|
+
const { sql, params } = buildCountQuery(table, this.schema, names.map((n) => this.filters[n]));
|
|
95
|
+
const databaseId = await findDatabase(this._context, table, this.schema);
|
|
96
|
+
const googleConf = this._context.asConfiguration();
|
|
97
|
+
const projectId = googleConf.getOrThrow('google.project');
|
|
98
|
+
const instanceName = googleConf.getOrThrow('google.spanner.instance.name');
|
|
99
|
+
this._context.logger.debug(`🗃️ Counting rows in Spanner database '${databaseId}' in instance '${instanceName}' (project '${projectId}'):\n${sql}`);
|
|
100
|
+
const spanner = new Spanner({ projectId });
|
|
101
|
+
let database;
|
|
102
|
+
try {
|
|
103
|
+
database = spanner.instance(instanceName).database(databaseId);
|
|
104
|
+
const [rows] = await database.run({
|
|
105
|
+
sql,
|
|
106
|
+
params,
|
|
107
|
+
json: true,
|
|
108
|
+
jsonOptions: { wrapNumbers: false },
|
|
109
|
+
});
|
|
110
|
+
const row = rows[0];
|
|
111
|
+
return {
|
|
112
|
+
total: Number(row.total),
|
|
113
|
+
counts: Object.fromEntries(names.map((name, index) => [name, Number(row[`f${index}`])])),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
await database?.close();
|
|
118
|
+
spanner.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DatabaseCountRecords, type RecordCounts } from '@causa/workspace-core';
|
|
2
|
+
/**
|
|
3
|
+
* Implements {@link DatabaseCountRecords} for Spanner databases.
|
|
4
|
+
*
|
|
5
|
+
* The table is the one in the schema's `google.spanner` binding, in the database whose DDL creates it. When several
|
|
6
|
+
* databases create a table with that name, the one in the domain of the schema is chosen. The Spanner instance and GCP
|
|
7
|
+
* project are read from the workspace configuration.
|
|
8
|
+
*
|
|
9
|
+
* Properties are read from the column named by their `googleSpannerColumn` extension, or from the column with the same
|
|
10
|
+
* name. Nested properties are read from JSON columns.
|
|
11
|
+
*/
|
|
12
|
+
export declare class DatabaseCountRecordsForSpanner extends DatabaseCountRecords {
|
|
13
|
+
_call(): Promise<RecordCounts>;
|
|
14
|
+
_supports(): boolean;
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { callDeferred } from '@causa/workspace';
|
|
2
|
+
import { DatabaseCountRecords } from '@causa/workspace-core';
|
|
3
|
+
import { SPANNER_ENGINE } from './query-records.js';
|
|
4
|
+
/**
|
|
5
|
+
* Implements {@link DatabaseCountRecords} for Spanner databases.
|
|
6
|
+
*
|
|
7
|
+
* The table is the one in the schema's `google.spanner` binding, in the database whose DDL creates it. When several
|
|
8
|
+
* databases create a table with that name, the one in the domain of the schema is chosen. The Spanner instance and GCP
|
|
9
|
+
* project are read from the workspace configuration.
|
|
10
|
+
*
|
|
11
|
+
* Properties are read from the column named by their `googleSpannerColumn` extension, or from the column with the same
|
|
12
|
+
* name. Nested properties are read from JSON columns.
|
|
13
|
+
*/
|
|
14
|
+
export class DatabaseCountRecordsForSpanner extends DatabaseCountRecords {
|
|
15
|
+
async _call() {
|
|
16
|
+
return await callDeferred(this, import.meta.url);
|
|
17
|
+
}
|
|
18
|
+
_supports() {
|
|
19
|
+
return this.engine === SPANNER_ENGINE;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
export { DatabaseCountRecordsForSpanner } from './count-records.js';
|
|
1
2
|
export { ModelSchemaExtractDatabaseForGoogleSpanner } from './extract-database.js';
|
|
2
3
|
export { ModelGenerateTypeScriptDecoratorsForGoogleSpanner } from './generate-typescript-decorators.js';
|
|
3
|
-
export { GoogleSpannerListDatabases } from './list-databases.js';
|
|
4
|
+
export { GoogleSpannerListDatabases, type SpannerDatabase, type SpannerTable, } from './list-databases.js';
|
|
4
5
|
export { DatabaseQueryRecordsForSpanner } from './query-records.js';
|
|
6
|
+
export { resolveSpannerTable, type SpannerTableResolution, } from './resolve-table.js';
|
|
5
7
|
export { GoogleSpannerWriteDatabases } from './write-databases.js';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
export { DatabaseCountRecordsForSpanner } from './count-records.js';
|
|
1
2
|
export { ModelSchemaExtractDatabaseForGoogleSpanner } from './extract-database.js';
|
|
2
3
|
export { ModelGenerateTypeScriptDecoratorsForGoogleSpanner } from './generate-typescript-decorators.js';
|
|
3
|
-
export { GoogleSpannerListDatabases } from './list-databases.js';
|
|
4
|
+
export { GoogleSpannerListDatabases, } from './list-databases.js';
|
|
4
5
|
export { DatabaseQueryRecordsForSpanner } from './query-records.js';
|
|
6
|
+
export { resolveSpannerTable, } from './resolve-table.js';
|
|
5
7
|
export { GoogleSpannerWriteDatabases } from './write-databases.js';
|
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import { WorkspaceFunction } from '@causa/workspace';
|
|
2
|
+
/**
|
|
3
|
+
* A table created by a `CREATE TABLE` statement in a Spanner DDL file.
|
|
4
|
+
*/
|
|
5
|
+
export type SpannerTable = {
|
|
6
|
+
/**
|
|
7
|
+
* The name of the table.
|
|
8
|
+
*/
|
|
9
|
+
readonly name: string;
|
|
10
|
+
/**
|
|
11
|
+
* The absolute path of the DDL file creating the table.
|
|
12
|
+
*/
|
|
13
|
+
readonly file: string;
|
|
14
|
+
/**
|
|
15
|
+
* The line of the statement in the file.
|
|
16
|
+
*/
|
|
17
|
+
readonly line: number;
|
|
18
|
+
};
|
|
2
19
|
/**
|
|
3
20
|
* The definition of a Spanner database with its DDL statements.
|
|
4
21
|
*/
|
|
@@ -15,6 +32,10 @@ export type SpannerDatabase = {
|
|
|
15
32
|
* The DDL statements for the database.
|
|
16
33
|
*/
|
|
17
34
|
ddls: string[];
|
|
35
|
+
/**
|
|
36
|
+
* The tables created by the `CREATE TABLE` statements of the DDL files.
|
|
37
|
+
*/
|
|
38
|
+
tables: SpannerTable[];
|
|
18
39
|
};
|
|
19
40
|
/**
|
|
20
41
|
* A function that lists Spanner databases defined in the workspace, and returns their corresponding DDLs.
|
|
@@ -26,7 +47,7 @@ export declare class GoogleSpannerListDatabases extends WorkspaceFunction<Promis
|
|
|
26
47
|
* End-of-the-line comments are removed, and statements are split using semicolons.
|
|
27
48
|
*
|
|
28
49
|
* @param path The path to the SQL file containing DDL statements.
|
|
29
|
-
* @returns The list of DDL statements.
|
|
50
|
+
* @returns The list of DDL statements, along with the file and the line at which each statement starts.
|
|
30
51
|
*/
|
|
31
52
|
private parseDdlFile;
|
|
32
53
|
_supports(): boolean;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { WorkspaceFunction, listFilesAndFormat } from '@causa/workspace';
|
|
2
2
|
import { readFile } from 'fs/promises';
|
|
3
|
+
/**
|
|
4
|
+
* Matches a `CREATE TABLE` DDL statement, capturing the name of the table.
|
|
5
|
+
*/
|
|
6
|
+
const CREATE_TABLE_REGEX = /^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"']?([A-Za-z_]\w*)/i;
|
|
3
7
|
/**
|
|
4
8
|
* The default format used to infer the ID / name of the database from the format parts.
|
|
5
9
|
*/
|
|
@@ -33,10 +37,14 @@ export class GoogleSpannerListDatabases extends WorkspaceFunction {
|
|
|
33
37
|
}, {});
|
|
34
38
|
return await Promise.all(Object.entries(databaseDdlFiles).map(async ([id, ddlFiles]) => {
|
|
35
39
|
ddlFiles.sort();
|
|
36
|
-
const
|
|
37
|
-
const ddls =
|
|
40
|
+
const statements = (await Promise.all(ddlFiles.map((ddlFile) => this.parseDdlFile(ddlFile)))).flat();
|
|
41
|
+
const ddls = statements.map(({ statement }) => statement);
|
|
42
|
+
const tables = statements.flatMap(({ statement, file, line }) => {
|
|
43
|
+
const name = statement.match(CREATE_TABLE_REGEX)?.[1];
|
|
44
|
+
return name ? [{ name, file, line }] : [];
|
|
45
|
+
});
|
|
38
46
|
this._context.logger.debug(`🗃️ Found Spanner database '${id}' with ${ddlFiles.length} DDL file(s) and ${ddls.length} statement(s).`);
|
|
39
|
-
return { id, ddlFiles, ddls };
|
|
47
|
+
return { id, ddlFiles, ddls, tables };
|
|
40
48
|
}));
|
|
41
49
|
}
|
|
42
50
|
/**
|
|
@@ -44,21 +52,25 @@ export class GoogleSpannerListDatabases extends WorkspaceFunction {
|
|
|
44
52
|
* End-of-the-line comments are removed, and statements are split using semicolons.
|
|
45
53
|
*
|
|
46
54
|
* @param path The path to the SQL file containing DDL statements.
|
|
47
|
-
* @returns The list of DDL statements.
|
|
55
|
+
* @returns The list of DDL statements, along with the file and the line at which each statement starts.
|
|
48
56
|
*/
|
|
49
57
|
async parseDdlFile(path) {
|
|
50
58
|
const rawFile = (await readFile(path)).toString();
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
.
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
// Removing comments using the `-- My comment until the end of the line` syntax, which preserves line numbers.
|
|
60
|
+
const withoutComments = rawFile.replace(/--.*$/gm, '');
|
|
61
|
+
let line = 1;
|
|
62
|
+
return withoutComments.split(';').map((rawStatement) => {
|
|
63
|
+
const leadingWhitespace = rawStatement.match(/^\s*/)[0];
|
|
64
|
+
const startLine = line + leadingWhitespace.split('\n').length - 1;
|
|
65
|
+
line += rawStatement.split('\n').length - 1;
|
|
66
|
+
const statement = rawStatement
|
|
67
|
+
// Trimming lines for readability in logs, and joining them into a single line.
|
|
68
|
+
.split('\n')
|
|
69
|
+
.map((statementLine) => statementLine.trim())
|
|
70
|
+
.join(' ')
|
|
71
|
+
.trim();
|
|
72
|
+
return { statement, file: path, line: startLine };
|
|
73
|
+
});
|
|
62
74
|
}
|
|
63
75
|
_supports() {
|
|
64
76
|
return true;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SpannerDatabase, SpannerTable } from './list-databases.js';
|
|
2
|
+
/**
|
|
3
|
+
* The resolution of a table name to the database whose DDL creates it.
|
|
4
|
+
*/
|
|
5
|
+
export type SpannerTableResolution<D extends SpannerDatabase = SpannerDatabase> = {
|
|
6
|
+
readonly kind: 'resolved';
|
|
7
|
+
readonly database: D;
|
|
8
|
+
readonly table: SpannerTable;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'ambiguous';
|
|
11
|
+
readonly candidates: D[];
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: 'missing';
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Resolves a table name to the database whose DDL creates it. When several databases create a table with that name, the
|
|
17
|
+
* one with DDL files in the domain of the schema is chosen.
|
|
18
|
+
*
|
|
19
|
+
* @param databases The Spanner databases, as listed by `GoogleSpannerListDatabases`.
|
|
20
|
+
* @param table The name of the table.
|
|
21
|
+
* @param schemaFile The absolute path of the file declaring the schema persisted in the table.
|
|
22
|
+
* @param domainPaths The absolute paths of the domains of the workspace.
|
|
23
|
+
* @returns The resolution.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveSpannerTable<D extends SpannerDatabase>(databases: readonly D[], table: string, schemaFile: string, domainPaths: readonly string[]): SpannerTableResolution<D>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { sep } from 'path';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the deepest domain containing a path.
|
|
4
|
+
*
|
|
5
|
+
* @param domainPaths The absolute paths of the domains.
|
|
6
|
+
* @param path The absolute path.
|
|
7
|
+
* @returns The absolute path of the domain, or `undefined` if the path is not part of a domain.
|
|
8
|
+
*/
|
|
9
|
+
function domainOf(domainPaths, path) {
|
|
10
|
+
return domainPaths
|
|
11
|
+
.filter((domain) => path.startsWith(`${domain}${sep}`))
|
|
12
|
+
.sort((a, b) => b.length - a.length)[0];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolves a table name to the database whose DDL creates it. When several databases create a table with that name, the
|
|
16
|
+
* one with DDL files in the domain of the schema is chosen.
|
|
17
|
+
*
|
|
18
|
+
* @param databases The Spanner databases, as listed by `GoogleSpannerListDatabases`.
|
|
19
|
+
* @param table The name of the table.
|
|
20
|
+
* @param schemaFile The absolute path of the file declaring the schema persisted in the table.
|
|
21
|
+
* @param domainPaths The absolute paths of the domains of the workspace.
|
|
22
|
+
* @returns The resolution.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveSpannerTable(databases, table, schemaFile, domainPaths) {
|
|
25
|
+
const candidates = databases.filter((d) => d.tables.some((t) => t.name === table));
|
|
26
|
+
const pick = (database) => ({
|
|
27
|
+
kind: 'resolved',
|
|
28
|
+
database,
|
|
29
|
+
table: database.tables.find((t) => t.name === table),
|
|
30
|
+
});
|
|
31
|
+
if (candidates.length === 1) {
|
|
32
|
+
return pick(candidates[0]);
|
|
33
|
+
}
|
|
34
|
+
if (candidates.length === 0) {
|
|
35
|
+
return { kind: 'missing' };
|
|
36
|
+
}
|
|
37
|
+
const domain = domainOf(domainPaths, schemaFile);
|
|
38
|
+
const inDomain = domain
|
|
39
|
+
? candidates.filter(({ ddlFiles }) => ddlFiles.some((f) => domainOf(domainPaths, f) === domain))
|
|
40
|
+
: [];
|
|
41
|
+
return inDomain.length === 1
|
|
42
|
+
? pick(inDomain[0])
|
|
43
|
+
: { kind: 'ambiguous', candidates };
|
|
44
|
+
}
|
package/dist/functions/index.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
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, EventTopicBrokerDeleteTriggerResourceForCloudRunService, EventTopicBrokerDeleteTriggerResourceForPubSubSubscription, EventTopicBrokerDeleteTriggerResourceForServiceAccount, EventTopicBrokerGetTopicIdForPubSub, EventTopicBrokerPublishEventsForGoogle, EventTopicBrokerWaitForProcessingForPubSub, EventTopicCreateBackfillSourceForBigQuery, EventTopicQueryEventsForBigQuery, } from './event-topic/index.js';
|
|
3
|
+
import { EventTopicBrokerCreateTopicForPubSub, EventTopicBrokerCreateTriggerForCloudRun, EventTopicBrokerDeleteTopicForPubSub, EventTopicBrokerDeleteTriggerResourceForCloudRunInvokerRole, EventTopicBrokerDeleteTriggerResourceForCloudRunService, EventTopicBrokerDeleteTriggerResourceForPubSubSubscription, EventTopicBrokerDeleteTriggerResourceForServiceAccount, EventTopicBrokerGetTopicIdForPubSub, EventTopicBrokerPublishEventsForGoogle, EventTopicBrokerWaitForProcessingForPubSub, EventTopicCountEventsForBigQuery, EventTopicCreateBackfillSourceForBigQuery, EventTopicQueryEventsForBigQuery, } 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 { DatabaseQueryRecordsForFirestore, GoogleFirestoreMergeRules, ModelGenerateTypeScriptDecoratorsForGoogleFirestore, ModelSchemaExtractDatabaseForGoogleFirestore, } from './google-firestore/index.js';
|
|
7
7
|
import { GoogleIdentityPlatformGenerateCustomToken, GoogleIdentityPlatformGenerateToken, } from './google-identity-platform/index.js';
|
|
8
8
|
import { GooglePubSubWriteTopics } from './google-pubsub/index.js';
|
|
9
9
|
import { GoogleServicesEnable } from './google-services/index.js';
|
|
10
|
-
import { DatabaseQueryRecordsForSpanner, GoogleSpannerListDatabases, GoogleSpannerWriteDatabases, ModelGenerateTypeScriptDecoratorsForGoogleSpanner, ModelSchemaExtractDatabaseForGoogleSpanner, } from './google-spanner/index.js';
|
|
10
|
+
import { DatabaseCountRecordsForSpanner, DatabaseQueryRecordsForSpanner, GoogleSpannerListDatabases, GoogleSpannerWriteDatabases, ModelGenerateTypeScriptDecoratorsForGoogleSpanner, ModelSchemaExtractDatabaseForGoogleSpanner, } from './google-spanner/index.js';
|
|
11
11
|
import { GraphFetchEnvironmentMetricSeriesForGoogle, GraphGetEnvironmentProviderForGoogle, GraphListRulesForGoogle, } from './graph/index.js';
|
|
12
12
|
import { ProjectGetArtefactDestinationForCloudFunctions, ProjectGetArtefactDestinationForCloudRun, ProjectPushArtefactForCloudFunctions, } from './project/index.js';
|
|
13
13
|
import { SecretFetchForGoogleAccessToken, SecretFetchForGoogleSecretManager, } from './secret/index.js';
|
|
14
14
|
import { ServiceContainerQueryLogsForCloudRun } from './service-container/index.js';
|
|
15
15
|
import { ModelGenerateTypeScriptTriggerDecoratorsForGoogle } from './typescript/index.js';
|
|
16
16
|
export function registerFunctions(context) {
|
|
17
|
-
context.registerFunctionImplementations(CausaListConfigurationSchemasForGoogle, DatabaseQueryRecordsForFirestore, DatabaseQueryRecordsForSpanner, EmulatorStartForFirebaseStorage, EmulatorStartForFirestore, EmulatorStartForIdentityPlatform, EmulatorStartForPubSub, EmulatorStartForSpanner, EmulatorStopForFirebaseStorage, EmulatorStopForFirestore, EmulatorStopForIdentityPlatform, EmulatorStopForPubSub, EmulatorStopForSpanner, EventTopicBrokerCreateTopicForPubSub, EventTopicBrokerCreateTriggerForCloudRun, EventTopicBrokerDeleteTopicForPubSub, EventTopicBrokerDeleteTriggerResourceForCloudRunInvokerRole, EventTopicBrokerDeleteTriggerResourceForCloudRunService, EventTopicBrokerDeleteTriggerResourceForPubSubSubscription, EventTopicBrokerDeleteTriggerResourceForServiceAccount, EventTopicBrokerGetTopicIdForPubSub, EventTopicBrokerPublishEventsForGoogle, EventTopicBrokerWaitForProcessingForPubSub, EventTopicCreateBackfillSourceForBigQuery, EventTopicQueryEventsForBigQuery, GoogleAppCheckGenerateToken, GoogleFirebaseStorageMergeRules, GoogleFirestoreMergeRules, GoogleIdentityPlatformGenerateCustomToken, GoogleIdentityPlatformGenerateToken, GooglePubSubWriteTopics, GoogleServicesEnable, GoogleSpannerListDatabases, GoogleSpannerWriteDatabases, GraphFetchEnvironmentMetricSeriesForGoogle, GraphGetEnvironmentProviderForGoogle, GraphListRulesForGoogle, ModelGenerateTypeScriptDecoratorsForGoogleFirestore, ModelGenerateTypeScriptDecoratorsForGoogleSpanner, ModelGenerateTypeScriptTriggerDecoratorsForGoogle, ModelSchemaExtractDatabaseForGoogleFirestore, ModelSchemaExtractDatabaseForGoogleSpanner, ProjectGetArtefactDestinationForCloudFunctions, ProjectGetArtefactDestinationForCloudRun, ProjectPushArtefactForCloudFunctions, SecretFetchForGoogleAccessToken, SecretFetchForGoogleSecretManager, ServiceContainerQueryLogsForCloudRun);
|
|
17
|
+
context.registerFunctionImplementations(CausaListConfigurationSchemasForGoogle, DatabaseCountRecordsForSpanner, DatabaseQueryRecordsForFirestore, DatabaseQueryRecordsForSpanner, EmulatorStartForFirebaseStorage, EmulatorStartForFirestore, EmulatorStartForIdentityPlatform, EmulatorStartForPubSub, EmulatorStartForSpanner, EmulatorStopForFirebaseStorage, EmulatorStopForFirestore, EmulatorStopForIdentityPlatform, EmulatorStopForPubSub, EmulatorStopForSpanner, EventTopicBrokerCreateTopicForPubSub, EventTopicBrokerCreateTriggerForCloudRun, EventTopicBrokerDeleteTopicForPubSub, EventTopicBrokerDeleteTriggerResourceForCloudRunInvokerRole, EventTopicBrokerDeleteTriggerResourceForCloudRunService, EventTopicBrokerDeleteTriggerResourceForPubSubSubscription, EventTopicBrokerDeleteTriggerResourceForServiceAccount, EventTopicBrokerGetTopicIdForPubSub, EventTopicBrokerPublishEventsForGoogle, EventTopicBrokerWaitForProcessingForPubSub, EventTopicCountEventsForBigQuery, EventTopicCreateBackfillSourceForBigQuery, EventTopicQueryEventsForBigQuery, GoogleAppCheckGenerateToken, GoogleFirebaseStorageMergeRules, GoogleFirestoreMergeRules, GoogleIdentityPlatformGenerateCustomToken, GoogleIdentityPlatformGenerateToken, GooglePubSubWriteTopics, GoogleServicesEnable, GoogleSpannerListDatabases, GoogleSpannerWriteDatabases, GraphFetchEnvironmentMetricSeriesForGoogle, GraphGetEnvironmentProviderForGoogle, GraphListRulesForGoogle, ModelGenerateTypeScriptDecoratorsForGoogleFirestore, ModelGenerateTypeScriptDecoratorsForGoogleSpanner, ModelGenerateTypeScriptTriggerDecoratorsForGoogle, ModelSchemaExtractDatabaseForGoogleFirestore, ModelSchemaExtractDatabaseForGoogleSpanner, ProjectGetArtefactDestinationForCloudFunctions, ProjectGetArtefactDestinationForCloudRun, ProjectPushArtefactForCloudFunctions, SecretFetchForGoogleAccessToken, SecretFetchForGoogleSecretManager, ServiceContainerQueryLogsForCloudRun);
|
|
18
18
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { GraphOriginKind, } from '@causa/workspace-core';
|
|
2
2
|
import { DomainsFact, ModelFact, projectId, ProjectsFact, topicId, triggerId, } from '@causa/workspace-core/graph';
|
|
3
3
|
import { FIRESTORE_ENGINE, firestoreCollectionPath, SPANNER_ENGINE, } from '../../ids.js';
|
|
4
|
-
import { ddlSource,
|
|
4
|
+
import { ddlSource, resolveEntitySpannerTable, SpannerFact, } from '../../spanner.js';
|
|
5
5
|
/**
|
|
6
6
|
* The elements declared by the Google-specific parts of the `serviceContainer` configuration of every service container
|
|
7
7
|
* project.
|
|
@@ -18,7 +18,7 @@ export const googleServiceContainerFromConfiguration = {
|
|
|
18
18
|
kind: GraphOriginKind.Declared,
|
|
19
19
|
description: 'One `delivers` edge from a topic to each trigger of type `google.pubSub` naming it, when an event schema defines the topic. One `accesses` edge from a project to each entity persisted in a Spanner database or a Firestore collection listed in its `serviceContainer.outputs`.',
|
|
20
20
|
async run(graph) {
|
|
21
|
-
const { locator } = graph;
|
|
21
|
+
const { locator, context } = graph;
|
|
22
22
|
const [domains, projects, model, spanner] = await Promise.all([
|
|
23
23
|
graph.get(DomainsFact),
|
|
24
24
|
graph.get(ProjectsFact),
|
|
@@ -29,7 +29,7 @@ export const googleServiceContainerFromConfiguration = {
|
|
|
29
29
|
const warnings = [];
|
|
30
30
|
const entities = [...model.entities.values()];
|
|
31
31
|
const spannerEntities = entities.flatMap((entity) => {
|
|
32
|
-
const resolution =
|
|
32
|
+
const resolution = resolveEntitySpannerTable(spanner, entity, domains);
|
|
33
33
|
return resolution?.kind === 'resolved' ? [{ entity, resolution }] : [];
|
|
34
34
|
});
|
|
35
35
|
for (const project of projects.filter((p) => p.type === 'serviceContainer')) {
|
|
@@ -78,7 +78,7 @@ export const googleServiceContainerFromConfiguration = {
|
|
|
78
78
|
type: 'accesses',
|
|
79
79
|
from: projectId(project.directory),
|
|
80
80
|
to: entity.id,
|
|
81
|
-
sources: [source, ddlSource(resolution.table)],
|
|
81
|
+
sources: [source, ddlSource(context.rootPath, resolution.table)],
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
}
|
|
@@ -2,7 +2,7 @@ import { GraphOriginKind, } from '@causa/workspace-core';
|
|
|
2
2
|
import { databaseId, tableId, tableLocator, DomainsFact, ModelFact, nodeId, } from '@causa/workspace-core/graph';
|
|
3
3
|
import { terraformDeploys } from '@causa/workspace-terraform';
|
|
4
4
|
import { SPANNER_ENGINE, spannerInstanceId } from '../../ids.js';
|
|
5
|
-
import { ddlSource,
|
|
5
|
+
import { ddlSource, resolveEntitySpannerTable, SpannerFact, } from '../../spanner.js';
|
|
6
6
|
import { blockResource, infrastructureModules, argumentOrConfiguration, } from '../../terraform-modules.js';
|
|
7
7
|
/**
|
|
8
8
|
* The source of the Causa Terraform module creating the Spanner instance and databases.
|
|
@@ -135,13 +135,13 @@ export const spannerDatabasesModule = {
|
|
|
135
135
|
locator: nodeLocator,
|
|
136
136
|
parent: databaseId(database.locator),
|
|
137
137
|
name: table.name,
|
|
138
|
-
sources: [ddlSource(table), ...blocks],
|
|
138
|
+
sources: [ddlSource(context.rootPath, table), ...blocks],
|
|
139
139
|
});
|
|
140
140
|
edges.push(...spannerDatabases.flatMap((module) => terraformDeploys(module.block, nodeId('table', nodeLocator))));
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
for (const entity of model.entities.values()) {
|
|
144
|
-
const resolution =
|
|
144
|
+
const resolution = resolveEntitySpannerTable(spanner, entity, domains);
|
|
145
145
|
if (!resolution) {
|
|
146
146
|
continue;
|
|
147
147
|
}
|
|
@@ -172,7 +172,7 @@ export const spannerDatabasesModule = {
|
|
|
172
172
|
type: 'realizes',
|
|
173
173
|
from: tableId(resolution.database.locator, resolution.table.name),
|
|
174
174
|
to: entity.id,
|
|
175
|
-
sources: [binding, ddlSource(resolution.table)],
|
|
175
|
+
sources: [binding, ddlSource(context.rootPath, resolution.table)],
|
|
176
176
|
});
|
|
177
177
|
}
|
|
178
178
|
return { nodes, edges, warnings };
|
package/dist/graph/spanner.d.ts
CHANGED
|
@@ -1,30 +1,10 @@
|
|
|
1
1
|
import type { GraphOriginSource } from '@causa/workspace-core';
|
|
2
2
|
import { GraphExtractionFact, type EntityFacts, type GraphContext, type GraphFactOutput, type WorkspaceDomain } from '@causa/workspace-core/graph';
|
|
3
|
+
import { type SpannerDatabase, type SpannerTable, type SpannerTableResolution } from '../functions/google-spanner/index.js';
|
|
3
4
|
/**
|
|
4
|
-
* A
|
|
5
|
+
* A Spanner database of the workspace, as `GoogleSpannerListDatabases` lists it, and its graph identifiers.
|
|
5
6
|
*/
|
|
6
|
-
export type
|
|
7
|
-
/**
|
|
8
|
-
* The name of the table.
|
|
9
|
-
*/
|
|
10
|
-
readonly name: string;
|
|
11
|
-
/**
|
|
12
|
-
* The DDL file declaring the table, relative to the workspace root.
|
|
13
|
-
*/
|
|
14
|
-
readonly file: string;
|
|
15
|
-
/**
|
|
16
|
-
* The line of the statement in the file.
|
|
17
|
-
*/
|
|
18
|
-
readonly line: number;
|
|
19
|
-
};
|
|
20
|
-
/**
|
|
21
|
-
* A Spanner database of the workspace, and the tables its DDL files create.
|
|
22
|
-
*/
|
|
23
|
-
export type SpannerDatabaseFacts = {
|
|
24
|
-
/**
|
|
25
|
-
* The ID of the database, as `GoogleSpannerListDatabases` returns it.
|
|
26
|
-
*/
|
|
27
|
-
readonly id: string;
|
|
7
|
+
export type SpannerDatabaseFacts = SpannerDatabase & {
|
|
28
8
|
/**
|
|
29
9
|
* The name of the database within its engine, `<instance>.<database>`, as `outputs['google.spanner']` lists it.
|
|
30
10
|
*/
|
|
@@ -37,14 +17,6 @@ export type SpannerDatabaseFacts = {
|
|
|
37
17
|
* The directory holding the DDL files, relative to the workspace root.
|
|
38
18
|
*/
|
|
39
19
|
readonly directory: string;
|
|
40
|
-
/**
|
|
41
|
-
* The domain the DDL files belong to.
|
|
42
|
-
*/
|
|
43
|
-
readonly domain: WorkspaceDomain | undefined;
|
|
44
|
-
/**
|
|
45
|
-
* The tables created by the DDL files.
|
|
46
|
-
*/
|
|
47
|
-
readonly tables: SpannerTable[];
|
|
48
20
|
};
|
|
49
21
|
/**
|
|
50
22
|
* The Spanner databases of the workspace.
|
|
@@ -60,47 +32,26 @@ export type SpannerFacts = {
|
|
|
60
32
|
readonly databases: SpannerDatabaseFacts[];
|
|
61
33
|
};
|
|
62
34
|
/**
|
|
63
|
-
* The Spanner databases of the workspace, as `GoogleSpannerListDatabases` lists them
|
|
64
|
-
* create.
|
|
35
|
+
* The Spanner databases of the workspace, as `GoogleSpannerListDatabases` lists them.
|
|
65
36
|
*/
|
|
66
37
|
export declare class SpannerFact extends GraphExtractionFact<SpannerFacts> {
|
|
67
38
|
compute(graph: GraphContext): Promise<GraphFactOutput<SpannerFacts>>;
|
|
68
|
-
/**
|
|
69
|
-
* Parses the tables created by the `CREATE TABLE` statements of DDL files.
|
|
70
|
-
*
|
|
71
|
-
* @param context The workspace context.
|
|
72
|
-
* @param files The DDL files, relative to the workspace root.
|
|
73
|
-
* @returns The tables, in the order of the files and statements.
|
|
74
|
-
*/
|
|
75
|
-
private parseTables;
|
|
76
39
|
}
|
|
77
40
|
/**
|
|
78
|
-
*
|
|
79
|
-
|
|
80
|
-
export type SpannerTableResolution = {
|
|
81
|
-
readonly kind: 'resolved';
|
|
82
|
-
readonly database: SpannerDatabaseFacts;
|
|
83
|
-
readonly table: SpannerTable;
|
|
84
|
-
} | {
|
|
85
|
-
readonly kind: 'ambiguous';
|
|
86
|
-
readonly candidates: SpannerDatabaseFacts[];
|
|
87
|
-
} | {
|
|
88
|
-
readonly kind: 'missing';
|
|
89
|
-
};
|
|
90
|
-
/**
|
|
91
|
-
* Resolves the Spanner binding of an entity to the database whose DDL creates a table of that name. When several
|
|
92
|
-
* databases declare the name, the one in the entity's domain is chosen.
|
|
41
|
+
* Resolves the Spanner binding of an entity to the database whose DDL creates a table of that name, using
|
|
42
|
+
* {@link resolveSpannerTable}.
|
|
93
43
|
*
|
|
94
44
|
* @param spanner The Spanner databases.
|
|
95
45
|
* @param entity The entity.
|
|
96
46
|
* @param domains The domains of the workspace, in which the domain of the entity is looked up.
|
|
97
47
|
* @returns The resolution, or `undefined` if the entity is not persisted in Spanner.
|
|
98
48
|
*/
|
|
99
|
-
export declare function
|
|
49
|
+
export declare function resolveEntitySpannerTable(spanner: SpannerFacts, entity: EntityFacts, domains: readonly WorkspaceDomain[]): SpannerTableResolution<SpannerDatabaseFacts> | undefined;
|
|
100
50
|
/**
|
|
101
51
|
* Returns the origin source of a `CREATE TABLE` statement.
|
|
102
52
|
*
|
|
53
|
+
* @param rootPath The absolute path of the workspace root.
|
|
103
54
|
* @param table The table.
|
|
104
55
|
* @returns The origin source.
|
|
105
56
|
*/
|
|
106
|
-
export declare function ddlSource(table: SpannerTable): GraphOriginSource;
|
|
57
|
+
export declare function ddlSource(rootPath: string, table: SpannerTable): GraphOriginSource;
|
package/dist/graph/spanner.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { dirname,
|
|
4
|
-
import { GoogleSpannerListDatabases } from '../functions/google-spanner/index.js';
|
|
1
|
+
import { GraphExtractionFact, } from '@causa/workspace-core/graph';
|
|
2
|
+
import { splitSchemaPath } from '@causa/workspace-core/jsonschema';
|
|
3
|
+
import { dirname, relative } from 'path';
|
|
4
|
+
import { GoogleSpannerListDatabases, resolveSpannerTable, } from '../functions/google-spanner/index.js';
|
|
5
5
|
import { SPANNER_ENGINE, spannerDatabaseLocator, spannerDatabaseName, } from './ids.js';
|
|
6
6
|
/**
|
|
7
|
-
* The Spanner databases of the workspace, as `GoogleSpannerListDatabases` lists them
|
|
8
|
-
* create.
|
|
7
|
+
* The Spanner databases of the workspace, as `GoogleSpannerListDatabases` lists them.
|
|
9
8
|
*/
|
|
10
9
|
export class SpannerFact extends GraphExtractionFact {
|
|
11
10
|
async compute(graph) {
|
|
@@ -13,23 +12,16 @@ export class SpannerFact extends GraphExtractionFact {
|
|
|
13
12
|
const instance = context
|
|
14
13
|
.asConfiguration()
|
|
15
14
|
.get('google.spanner.instance.name');
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
]);
|
|
20
|
-
const databases = await Promise.all(listed.map(async ({ id, ddlFiles }) => {
|
|
21
|
-
const files = ddlFiles.map((f) => relative(context.rootPath, f)).sort();
|
|
22
|
-
const tables = await this.parseTables(context, files);
|
|
23
|
-
const directory = files[0] ? dirname(files[0]) : '';
|
|
15
|
+
const listed = await context.call(GoogleSpannerListDatabases, {});
|
|
16
|
+
const databases = listed.map((database) => {
|
|
17
|
+
const [file] = database.ddlFiles;
|
|
24
18
|
return {
|
|
25
|
-
|
|
26
|
-
name: spannerDatabaseName(instance ?? '', id),
|
|
27
|
-
locator: spannerDatabaseLocator(instance ?? '', id),
|
|
28
|
-
directory,
|
|
29
|
-
domain: directory ? domainAt(domains, directory) : undefined,
|
|
30
|
-
tables,
|
|
19
|
+
...database,
|
|
20
|
+
name: spannerDatabaseName(instance ?? '', database.id),
|
|
21
|
+
locator: spannerDatabaseLocator(instance ?? '', database.id),
|
|
22
|
+
directory: file ? dirname(relative(context.rootPath, file)) : '',
|
|
31
23
|
};
|
|
32
|
-
})
|
|
24
|
+
});
|
|
33
25
|
databases.sort((a, b) => a.id.localeCompare(b.id));
|
|
34
26
|
const warnings = [];
|
|
35
27
|
for (const database of databases) {
|
|
@@ -38,7 +30,7 @@ export class SpannerFact extends GraphExtractionFact {
|
|
|
38
30
|
if (names.has(table.name)) {
|
|
39
31
|
warnings.push({
|
|
40
32
|
message: `Table '${table.name}' is created twice in database '${database.id}'.`,
|
|
41
|
-
sources: [ddlSource(table)],
|
|
33
|
+
sources: [ddlSource(context.rootPath, table)],
|
|
42
34
|
});
|
|
43
35
|
}
|
|
44
36
|
names.add(table.name);
|
|
@@ -46,72 +38,36 @@ export class SpannerFact extends GraphExtractionFact {
|
|
|
46
38
|
}
|
|
47
39
|
return { value: { instance, databases }, warnings };
|
|
48
40
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Parses the tables created by the `CREATE TABLE` statements of DDL files.
|
|
51
|
-
*
|
|
52
|
-
* @param context The workspace context.
|
|
53
|
-
* @param files The DDL files, relative to the workspace root.
|
|
54
|
-
* @returns The tables, in the order of the files and statements.
|
|
55
|
-
*/
|
|
56
|
-
async parseTables(context, files) {
|
|
57
|
-
const tables = [];
|
|
58
|
-
for (const file of files) {
|
|
59
|
-
const content = await readFile(join(context.rootPath, file), 'utf-8');
|
|
60
|
-
const withoutComments = content.replace(/--.*$/gm, (m) => ' '.repeat(m.length));
|
|
61
|
-
const statements = withoutComments.matchAll(/\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"']?([A-Za-z_]\w*)[`"']?/gi);
|
|
62
|
-
for (const match of statements) {
|
|
63
|
-
tables.push({
|
|
64
|
-
name: match[1],
|
|
65
|
-
file,
|
|
66
|
-
line: withoutComments.slice(0, match.index).split('\n').length,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return tables;
|
|
71
|
-
}
|
|
72
41
|
}
|
|
73
42
|
/**
|
|
74
|
-
* Resolves the Spanner binding of an entity to the database whose DDL creates a table of that name
|
|
75
|
-
*
|
|
43
|
+
* Resolves the Spanner binding of an entity to the database whose DDL creates a table of that name, using
|
|
44
|
+
* {@link resolveSpannerTable}.
|
|
76
45
|
*
|
|
77
46
|
* @param spanner The Spanner databases.
|
|
78
47
|
* @param entity The entity.
|
|
79
48
|
* @param domains The domains of the workspace, in which the domain of the entity is looked up.
|
|
80
49
|
* @returns The resolution, or `undefined` if the entity is not persisted in Spanner.
|
|
81
50
|
*/
|
|
82
|
-
export function
|
|
83
|
-
const { definition,
|
|
51
|
+
export function resolveEntitySpannerTable(spanner, entity, domains) {
|
|
52
|
+
const { definition, path } = entity.schema;
|
|
84
53
|
const table = definition.databases.find((d) => d.engine === SPANNER_ENGINE)?.table;
|
|
85
54
|
if (!table) {
|
|
86
55
|
return undefined;
|
|
87
56
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
database,
|
|
92
|
-
table: database.tables.find((t) => t.name === table),
|
|
93
|
-
});
|
|
94
|
-
if (candidates.length === 1) {
|
|
95
|
-
return pick(candidates[0]);
|
|
96
|
-
}
|
|
97
|
-
if (candidates.length === 0) {
|
|
98
|
-
return { kind: 'missing' };
|
|
99
|
-
}
|
|
100
|
-
const domain = domainOfFile(domains, file);
|
|
101
|
-
const sameDomain = candidates.filter((d) => domain && d.domain?.directory === domain.directory);
|
|
102
|
-
return sameDomain.length === 1
|
|
103
|
-
? pick(sameDomain[0])
|
|
104
|
-
: { kind: 'ambiguous', candidates };
|
|
57
|
+
return resolveSpannerTable(spanner.databases, table, splitSchemaPath(path).file,
|
|
58
|
+
// The working directory of a domain's context is the absolute path of the domain.
|
|
59
|
+
domains.map((d) => d.context.workingDirectory));
|
|
105
60
|
}
|
|
106
61
|
/**
|
|
107
62
|
* Returns the origin source of a `CREATE TABLE` statement.
|
|
108
63
|
*
|
|
64
|
+
* @param rootPath The absolute path of the workspace root.
|
|
109
65
|
* @param table The table.
|
|
110
66
|
* @returns The origin source.
|
|
111
67
|
*/
|
|
112
|
-
export function ddlSource(table) {
|
|
68
|
+
export function ddlSource(rootPath, table) {
|
|
113
69
|
return {
|
|
114
|
-
path: table.file,
|
|
70
|
+
path: relative(rootPath, table.file),
|
|
115
71
|
pointer: `CREATE TABLE ${table.name}`,
|
|
116
72
|
location: { start: { line: table.line } },
|
|
117
73
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@causa/workspace-google",
|
|
3
|
-
"version": "1.4.0-beta.
|
|
3
|
+
"version": "1.4.0-beta.3",
|
|
4
4
|
"description": "The Causa workspace module providing many functionalities related to GCP and its services.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,28 +37,28 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@causa/cli": "^1.0.1-beta.1",
|
|
39
39
|
"@causa/workspace": "^1.3.0-beta.1",
|
|
40
|
-
"@causa/workspace-core": "^1.5.0-beta.
|
|
40
|
+
"@causa/workspace-core": "^1.5.0-beta.3",
|
|
41
41
|
"@causa/workspace-terraform": "^1.1.0-beta.2",
|
|
42
42
|
"@causa/workspace-typescript": "^1.4.0-beta.1",
|
|
43
43
|
"@google-cloud/apikeys": "^3.1.1",
|
|
44
|
-
"@google-cloud/bigquery": "^9.
|
|
44
|
+
"@google-cloud/bigquery": "^9.2.0",
|
|
45
45
|
"@google-cloud/iam-credentials": "^5.0.1",
|
|
46
|
-
"@google-cloud/logging": "^12.
|
|
46
|
+
"@google-cloud/logging": "^12.2.0",
|
|
47
47
|
"@google-cloud/monitoring": "^6.1.1",
|
|
48
|
-
"@google-cloud/pubsub": "^6.
|
|
48
|
+
"@google-cloud/pubsub": "^6.2.0",
|
|
49
49
|
"@google-cloud/resource-manager": "^7.1.1",
|
|
50
50
|
"@google-cloud/run": "^4.1.1",
|
|
51
51
|
"@google-cloud/secret-manager": "^7.1.1",
|
|
52
52
|
"@google-cloud/service-usage": "^5.1.1",
|
|
53
53
|
"@google-cloud/spanner": "^9.0.0",
|
|
54
|
-
"@google-cloud/storage": "^8.
|
|
54
|
+
"@google-cloud/storage": "^8.4.0",
|
|
55
55
|
"class-transformer": "^0.5.1",
|
|
56
56
|
"class-validator": "^0.15.1",
|
|
57
|
-
"firebase": "^
|
|
57
|
+
"firebase": "^13.0.0",
|
|
58
58
|
"firebase-admin": "^14.5.0",
|
|
59
59
|
"globby": "^16.2.4",
|
|
60
60
|
"google-gax": "5.0.6",
|
|
61
|
-
"googleapis": "^
|
|
61
|
+
"googleapis": "^185.0.0",
|
|
62
62
|
"micromatch": "^4.0.8",
|
|
63
63
|
"pino": "^10.4.0"
|
|
64
64
|
},
|