@causa/runtime-google 1.1.0 → 1.3.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.
@@ -114,6 +114,8 @@ export declare class SpannerEntityManager {
114
114
  /**
115
115
  * Returns the (quoted) name of the table for the given entity type.
116
116
  *
117
+ * @deprecated Use {@link sqlTable} instead.
118
+ *
117
119
  * @param entityTypeOrTable The type of entity, or the unquoted table name.
118
120
  * @param options Options when constructing the table name (e.g. the index to use).
119
121
  * @returns The name of the table, quoted with backticks.
@@ -130,6 +132,25 @@ export declare class SpannerEntityManager {
130
132
  */
131
133
  disableQueryNullFilteredIndexEmulatorCheck?: boolean;
132
134
  }): string;
135
+ /**
136
+ * Returns the (quoted) name of the table for the given entity type.
137
+ *
138
+ * @param entityTypeOrTable The type of entity, or the unquoted table name.
139
+ * @param options Options when constructing the table name (e.g. the index to use).
140
+ * @returns The name of the table, quoted with backticks.
141
+ */
142
+ sqlTable(entityTypeOrTable: Type | string, options?: {
143
+ /**
144
+ * Sets a table hint to indicate which index to use when querying the table.
145
+ * The value will be quoted with backticks.
146
+ */
147
+ index?: string;
148
+ /**
149
+ * Sets a table hint to disable the check that prevents queries from using null-filtered indexes.
150
+ * This is useful when using the emulator, which does not support null-filtered indexes.
151
+ */
152
+ disableQueryNullFilteredIndexEmulatorCheck?: boolean;
153
+ }): string;
133
154
  /**
134
155
  * Returns the (quoted) list of columns for the given entity type or list of columns.
135
156
  *
@@ -139,7 +160,16 @@ export declare class SpannerEntityManager {
139
160
  * @param entityTypeOrColumns The type of entity, or the unquoted list of columns.
140
161
  * @returns The list of columns, quoted with backticks and joined.
141
162
  */
142
- sqlColumns(entityTypeOrColumns: Type | string[]): string;
163
+ sqlColumns<T = unknown>(entityTypeOrColumns: Type<T> | string[], options?: {
164
+ /**
165
+ * If `entityTypeOrColumns` is a type, only the columns for the given properties will be included.
166
+ */
167
+ forProperties?: T extends object ? (keyof T & string)[] : never;
168
+ /**
169
+ * The alias with which to prefix each column.
170
+ */
171
+ alias?: string;
172
+ }): string;
143
173
  /**
144
174
  * Fetches a single row from the database using its key (either primary or for a secondary index).
145
175
  *
@@ -73,11 +73,23 @@ let SpannerEntityManager = class SpannerEntityManager {
73
73
  /**
74
74
  * Returns the (quoted) name of the table for the given entity type.
75
75
  *
76
+ * @deprecated Use {@link sqlTable} instead.
77
+ *
76
78
  * @param entityTypeOrTable The type of entity, or the unquoted table name.
77
79
  * @param options Options when constructing the table name (e.g. the index to use).
78
80
  * @returns The name of the table, quoted with backticks.
79
81
  */
80
82
  sqlTableName(entityTypeOrTable, options = {}) {
83
+ return this.sqlTable(entityTypeOrTable, options);
84
+ }
85
+ /**
86
+ * Returns the (quoted) name of the table for the given entity type.
87
+ *
88
+ * @param entityTypeOrTable The type of entity, or the unquoted table name.
89
+ * @param options Options when constructing the table name (e.g. the index to use).
90
+ * @returns The name of the table, quoted with backticks.
91
+ */
92
+ sqlTable(entityTypeOrTable, options = {}) {
81
93
  const tableHints = {};
82
94
  if (options.index) {
83
95
  tableHints.FORCE_INDEX = `\`${options.index}\``;
@@ -89,9 +101,10 @@ let SpannerEntityManager = class SpannerEntityManager {
89
101
  const tableHintsString = Object.entries(tableHints)
90
102
  .map(([k, v]) => `${k}=${v}`)
91
103
  .join(',');
92
- const quotedTableName = typeof entityTypeOrTable === 'string'
93
- ? `\`${entityTypeOrTable}\``
94
- : this.tableCache.getMetadata(entityTypeOrTable).quotedTableName;
104
+ const tableName = typeof entityTypeOrTable === 'string'
105
+ ? entityTypeOrTable
106
+ : this.tableCache.getMetadata(entityTypeOrTable).tableName;
107
+ const quotedTableName = `\`${tableName}\``;
95
108
  return tableHintsString.length > 0
96
109
  ? `${quotedTableName}@{${tableHintsString}}`
97
110
  : quotedTableName;
@@ -105,11 +118,22 @@ let SpannerEntityManager = class SpannerEntityManager {
105
118
  * @param entityTypeOrColumns The type of entity, or the unquoted list of columns.
106
119
  * @returns The list of columns, quoted with backticks and joined.
107
120
  */
108
- sqlColumns(entityTypeOrColumns) {
121
+ sqlColumns(entityTypeOrColumns, options = {}) {
122
+ let columns;
109
123
  if (Array.isArray(entityTypeOrColumns)) {
110
- return entityTypeOrColumns.map((c) => `\`${c}\``).join(', ');
124
+ columns = entityTypeOrColumns;
111
125
  }
112
- return this.tableCache.getMetadata(entityTypeOrColumns).quotedColumns;
126
+ else {
127
+ const { columnNames } = this.tableCache.getMetadata(entityTypeOrColumns);
128
+ columns = options.forProperties
129
+ ? options.forProperties.map((p) => columnNames[p])
130
+ : Object.values(columnNames);
131
+ }
132
+ columns = columns.map((c) => `\`${c}\``);
133
+ if (options.alias) {
134
+ columns = columns.map((c) => `\`${options.alias}\`.${c}`);
135
+ }
136
+ return columns.join(', ');
113
137
  }
114
138
  /**
115
139
  * Fetches a single row from the database using its key (either primary or for a secondary index).
@@ -131,8 +155,9 @@ let SpannerEntityManager = class SpannerEntityManager {
131
155
  if (!Array.isArray(key)) {
132
156
  key = [key];
133
157
  }
134
- const { tableName, columns: allColumns, primaryKeyColumns, softDeleteColumn, } = this.tableCache.getMetadata(entityType);
135
- const columns = options.columns ?? (options.index ? primaryKeyColumns : allColumns);
158
+ const { tableName, columnNames, primaryKeyColumns, softDeleteColumn } = this.tableCache.getMetadata(entityType);
159
+ const columns = options.columns ??
160
+ (options.index ? primaryKeyColumns : Object.values(columnNames));
136
161
  return await this.snapshot({ transaction: options.transaction }, async (transaction) => {
137
162
  const [rows] = await transaction.read(tableName, {
138
163
  keys: [key],
@@ -266,10 +291,8 @@ let SpannerEntityManager = class SpannerEntityManager {
266
291
  * @param options The options to use when running the operation.
267
292
  */
268
293
  async clear(entityType, options = {}) {
269
- const { quotedTableName } = this.tableCache.getMetadata(entityType);
270
- await this.transaction(options, (transaction) => transaction.runUpdate({
271
- sql: `DELETE FROM ${quotedTableName} WHERE TRUE`,
272
- }));
294
+ const { tableName } = this.tableCache.getMetadata(entityType);
295
+ await this.transaction(options, (transaction) => transaction.runUpdate(`DELETE FROM \`${tableName}\` WHERE TRUE`));
273
296
  }
274
297
  async query(optionsOrStatement, statement) {
275
298
  const options = statement
@@ -1,7 +1,7 @@
1
1
  export { SpannerColumn } from './column.decorator.js';
2
- export { SPANNER_SESSION_POOL_OPTIONS_FOR_CLOUD_FUNCTIONS, SPANNER_SESSION_POOL_OPTIONS_FOR_SERVICE, catchSpannerDatabaseErrors, getDefaultSpannerDatabaseForCloudFunction, } from './database.js';
2
+ export { catchSpannerDatabaseErrors, getDefaultSpannerDatabaseForCloudFunction, SPANNER_SESSION_POOL_OPTIONS_FOR_CLOUD_FUNCTIONS, SPANNER_SESSION_POOL_OPTIONS_FOR_SERVICE, } from './database.js';
3
3
  export { SpannerEntityManager } from './entity-manager.js';
4
- export type { SpannerKey, SpannerReadOnlyTransaction, SpannerReadWriteTransaction, } from './entity-manager.js';
4
+ export type { SpannerKey, SpannerReadOnlyTransaction, SpannerReadWriteTransaction, SqlStatement, } from './entity-manager.js';
5
5
  export * from './errors.js';
6
6
  export { SpannerHealthIndicator } from './healthcheck.js';
7
7
  export { SpannerModule } from './module.js';
@@ -1,5 +1,5 @@
1
1
  export { SpannerColumn } from './column.decorator.js';
2
- export { SPANNER_SESSION_POOL_OPTIONS_FOR_CLOUD_FUNCTIONS, SPANNER_SESSION_POOL_OPTIONS_FOR_SERVICE, catchSpannerDatabaseErrors, getDefaultSpannerDatabaseForCloudFunction, } from './database.js';
2
+ export { catchSpannerDatabaseErrors, getDefaultSpannerDatabaseForCloudFunction, SPANNER_SESSION_POOL_OPTIONS_FOR_CLOUD_FUNCTIONS, SPANNER_SESSION_POOL_OPTIONS_FOR_SERVICE, } from './database.js';
3
3
  export { SpannerEntityManager } from './entity-manager.js';
4
4
  export * from './errors.js';
5
5
  export { SpannerHealthIndicator } from './healthcheck.js';
@@ -7,22 +7,14 @@ export type CachedSpannerTableMetadata = {
7
7
  * The name of the table.
8
8
  */
9
9
  tableName: string;
10
- /**
11
- * The name of the table, quoted with backticks for use in queries.
12
- */
13
- quotedTableName: string;
14
10
  /**
15
11
  * The (ordered) list of columns that are part of the primary key.
16
12
  */
17
13
  primaryKeyColumns: string[];
18
14
  /**
19
- * The list of all columns in the table.
20
- */
21
- columns: string[];
22
- /**
23
- * The list of all columns in the table, quoted with backticks and joined for use in queries.
15
+ * A map from class property names to Spanner column names.
24
16
  */
25
- quotedColumns: string;
17
+ columnNames: Record<string, string>;
26
18
  /**
27
19
  * The name of the column used to mark soft deletes, if any.
28
20
  */
@@ -1,4 +1,4 @@
1
- import { getSpannerColumns, getSpannerColumnsMetadata, } from './column.decorator.js';
1
+ import { getSpannerColumnsMetadata } from './column.decorator.js';
2
2
  import { InvalidEntityDefinitionError } from './errors.js';
3
3
  import { getSpannerTableMetadataFromType } from './table.decorator.js';
4
4
  /**
@@ -21,7 +21,6 @@ export class SpannerTableCache {
21
21
  throw new InvalidEntityDefinitionError(entityType);
22
22
  }
23
23
  const tableName = tableMetadata.name;
24
- const quotedTableName = `\`${tableName}\``;
25
24
  const primaryKeyColumns = tableMetadata.primaryKey;
26
25
  const columnsMetadata = getSpannerColumnsMetadata(entityType);
27
26
  const softDeleteColumns = Object.values(columnsMetadata).filter((metadata) => metadata.softDelete);
@@ -29,15 +28,12 @@ export class SpannerTableCache {
29
28
  throw new InvalidEntityDefinitionError(entityType, `Only one column can be marked as soft delete.`);
30
29
  }
31
30
  const softDeleteColumn = softDeleteColumns[0]?.name ?? null;
32
- const columns = getSpannerColumns(entityType);
33
- const quotedColumns = columns.map((c) => `\`${c}\``).join(', ');
31
+ const columnNames = Object.fromEntries(Object.entries(columnsMetadata).map(([prop, { name }]) => [prop, name]));
34
32
  return {
35
33
  tableName,
36
- quotedTableName,
37
34
  primaryKeyColumns,
38
- columns,
39
- quotedColumns,
40
35
  softDeleteColumn,
36
+ columnNames,
41
37
  };
42
38
  }
43
39
  /**
@@ -1,11 +1,21 @@
1
1
  import type { ReadOnlyStateTransaction, ReadOnlyTransactionOption } from '@causa/runtime';
2
2
  import type { Type } from '@nestjs/common';
3
3
  import { Transaction } from 'firebase-admin/firestore';
4
+ import { type SoftDeletedFirestoreCollectionMetadata } from './soft-deleted-collection.decorator.js';
4
5
  import type { FirestoreCollectionResolver } from './types.js';
5
6
  /**
6
7
  * Option for a function that accepts a {@link FirestoreReadOnlyStateTransaction}.
7
8
  */
8
9
  export type FirestoreReadOnlyStateTransactionOption = ReadOnlyTransactionOption<FirestoreReadOnlyStateTransaction>;
10
+ /**
11
+ * Information about soft-deletion for a given document.
12
+ */
13
+ export type SoftDeleteInfo<T extends object> = Omit<SoftDeletedFirestoreCollectionMetadata, 'deletedDocumentsCollectionSuffix'> & {
14
+ /**
15
+ * The reference to the soft-deleted document.
16
+ */
17
+ ref: FirebaseFirestore.DocumentReference<T>;
18
+ };
9
19
  /**
10
20
  * A {@link ReadOnlyStateTransaction} that uses Firestore for state storage.
11
21
  *
@@ -33,5 +43,14 @@ export declare class FirestoreReadOnlyStateTransaction implements ReadOnlyStateT
33
43
  * The resolver that provides the Firestore collections for a given document type.
34
44
  */
35
45
  collectionResolver: FirestoreCollectionResolver);
46
+ /**
47
+ * Returns the soft-delete information for a given document, if the document type supports soft-deletion.
48
+ *
49
+ * @param activeDocRef The reference to the active document.
50
+ * @param type The type of the document.
51
+ * @returns The soft-delete information for the document, or `null` if the document type does not support
52
+ * soft-deletion.
53
+ */
54
+ protected getSoftDeleteInfo<T extends object>(activeDocRef: FirebaseFirestore.DocumentReference<T>, type: Type<T>): SoftDeleteInfo<T> | null;
36
55
  get<T extends object>(type: Type<T>, entity: Partial<T>): Promise<T | null>;
37
56
  }
@@ -1,5 +1,6 @@
1
1
  import { Transaction } from 'firebase-admin/firestore';
2
- import { getReferenceForFirestoreDocument } from '../../firestore/index.js';
2
+ import { getReferenceForFirestoreDocument, makeFirestoreDataConverter, } from '../../firestore/index.js';
3
+ import { getSoftDeletedFirestoreCollectionMetadataForType, } from './soft-deleted-collection.decorator.js';
3
4
  /**
4
5
  * A {@link ReadOnlyStateTransaction} that uses Firestore for state storage.
5
6
  *
@@ -24,24 +25,44 @@ export class FirestoreReadOnlyStateTransaction {
24
25
  this.firestoreTransaction = firestoreTransaction;
25
26
  this.collectionResolver = collectionResolver;
26
27
  }
28
+ /**
29
+ * Returns the soft-delete information for a given document, if the document type supports soft-deletion.
30
+ *
31
+ * @param activeDocRef The reference to the active document.
32
+ * @param type The type of the document.
33
+ * @returns The soft-delete information for the document, or `null` if the document type does not support
34
+ * soft-deletion.
35
+ */
36
+ getSoftDeleteInfo(activeDocRef, type) {
37
+ const softDeleteMetadata = getSoftDeletedFirestoreCollectionMetadataForType(type);
38
+ if (!softDeleteMetadata) {
39
+ return null;
40
+ }
41
+ const { deletedDocumentsCollectionSuffix: suffix, ...info } = softDeleteMetadata;
42
+ const softDeleteCollection = activeDocRef.firestore
43
+ .collection(`${activeDocRef.parent.path}${suffix}`)
44
+ .withConverter(makeFirestoreDataConverter(type));
45
+ const ref = softDeleteCollection.doc(activeDocRef.id);
46
+ return { ...info, ref };
47
+ }
27
48
  async get(type, entity) {
28
- const { activeCollection, softDelete } = this.collectionResolver.getCollectionsForType(type);
49
+ const { activeCollection } = this.collectionResolver.getCollectionsForType(type);
29
50
  const activeDocRef = getReferenceForFirestoreDocument(activeCollection, entity, type);
30
51
  const activeSnapshot = await this.firestoreTransaction.get(activeDocRef);
31
52
  const activeDocument = activeSnapshot.data();
32
53
  if (activeDocument) {
33
54
  return activeDocument;
34
55
  }
35
- if (!softDelete) {
56
+ const softDeleteInfo = this.getSoftDeleteInfo(activeDocRef, type);
57
+ if (!softDeleteInfo) {
36
58
  return null;
37
59
  }
38
- const deletedDocRef = getReferenceForFirestoreDocument(softDelete.collection, entity, type);
39
- const deletedSnapshot = await this.firestoreTransaction.get(deletedDocRef);
60
+ const deletedSnapshot = await this.firestoreTransaction.get(softDeleteInfo.ref);
40
61
  const deletedDocument = deletedSnapshot.data();
41
62
  if (!deletedDocument) {
42
63
  return null;
43
64
  }
44
- delete deletedDocument[softDelete.expirationField];
65
+ delete deletedDocument[softDeleteInfo.expirationField];
45
66
  return deletedDocument;
46
67
  }
47
68
  }
@@ -22,26 +22,26 @@ export class FirestoreStateTransaction extends FirestoreReadOnlyStateTransaction
22
22
  async delete(typeOrEntity, key) {
23
23
  const type = (key === undefined ? typeOrEntity.constructor : typeOrEntity);
24
24
  key ??= typeOrEntity;
25
- const { activeCollection, softDelete } = this.collectionResolver.getCollectionsForType(type);
25
+ const { activeCollection } = this.collectionResolver.getCollectionsForType(type);
26
26
  const activeDocRef = getReferenceForFirestoreDocument(activeCollection, key, type);
27
27
  this.firestoreTransaction.delete(activeDocRef);
28
- if (!softDelete) {
28
+ const softDeleteInfo = this.getSoftDeleteInfo(activeDocRef, type);
29
+ if (!softDeleteInfo) {
29
30
  return;
30
31
  }
31
- const deletedDocRef = getReferenceForFirestoreDocument(softDelete.collection, key, type);
32
- this.firestoreTransaction.delete(deletedDocRef);
32
+ this.firestoreTransaction.delete(softDeleteInfo.ref);
33
33
  }
34
34
  async set(entity) {
35
35
  const documentType = entity.constructor;
36
- const { activeCollection, softDelete } = this.collectionResolver.getCollectionsForType(documentType);
36
+ const { activeCollection } = this.collectionResolver.getCollectionsForType(documentType);
37
37
  const activeDocRef = getReferenceForFirestoreDocument(activeCollection, entity);
38
- if (!softDelete) {
38
+ const softDeleteInfo = this.getSoftDeleteInfo(activeDocRef, documentType);
39
+ if (!softDeleteInfo) {
39
40
  this.firestoreTransaction.set(activeDocRef, entity);
40
41
  return;
41
42
  }
42
- const deletedDocRef = getReferenceForFirestoreDocument(softDelete.collection, entity);
43
+ const { ref: deletedDocRef, expirationDelay, expirationField, } = softDeleteInfo;
43
44
  if ('deletedAt' in entity && entity.deletedAt instanceof Date) {
44
- const { expirationDelay, expirationField } = softDelete;
45
45
  const expiresAt = new Date(entity.deletedAt.getTime() + expirationDelay);
46
46
  const deletedDoc = plainToInstance(documentType, {
47
47
  ...entity,
@@ -12,6 +12,8 @@ export type FirestoreCollectionsForDocumentType<T> = {
12
12
  /**
13
13
  * Configuration about the soft-delete collection, where documents are stored when their `deletedAt` field is not
14
14
  * `null`. This can be `null` if the document type does not declare a soft-delete collection.
15
+ *
16
+ * @deprecated Use `SoftDeleteInfo` in `FirestoreReadOnlyStateTransaction.getSoftDeleteInfo` instead.
15
17
  */
16
18
  readonly softDelete: ({
17
19
  /**
@@ -99,8 +99,10 @@ export class SpannerOutboxSender extends OutboxEventSender {
99
99
  * @returns The SQL queries used to fetch and update events in the outbox.
100
100
  */
101
101
  buildSql() {
102
- const table = this.entityManager.sqlTableName(this.outboxEventType);
103
- const tableWithIndex = this.entityManager.sqlTableName(this.outboxEventType, { index: this.index });
102
+ const table = this.entityManager.sqlTable(this.outboxEventType);
103
+ const tableWithIndex = this.entityManager.sqlTable(this.outboxEventType, {
104
+ index: this.index,
105
+ });
104
106
  const noLeaseFilter = `\`${this.leaseExpirationColumn}\` IS NULL OR \`${this.leaseExpirationColumn}\` < @currentTime`;
105
107
  let fetchFilter = noLeaseFilter;
106
108
  if (this.sharding) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@causa/runtime-google",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "An extension to the Causa runtime SDK (`@causa/runtime`), providing Google-specific features.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,48 +32,48 @@
32
32
  "test:cov": "npm run test -- --coverage"
33
33
  },
34
34
  "dependencies": {
35
- "@causa/runtime": "^1.0.0",
35
+ "@causa/runtime": "^1.3.0",
36
36
  "@google-cloud/precise-date": "^5.0.0",
37
- "@google-cloud/pubsub": "^5.1.0",
38
- "@google-cloud/spanner": "^8.1.0",
39
- "@google-cloud/tasks": "^6.2.0",
40
- "@grpc/grpc-js": "^1.13.4",
41
- "@nestjs/common": "^11.1.5",
37
+ "@google-cloud/pubsub": "^5.2.0",
38
+ "@google-cloud/spanner": "^8.2.2",
39
+ "@google-cloud/tasks": "^6.2.1",
40
+ "@grpc/grpc-js": "^1.14.0",
41
+ "@nestjs/common": "^11.1.6",
42
42
  "@nestjs/config": "^4.0.2",
43
- "@nestjs/core": "^11.1.5",
43
+ "@nestjs/core": "^11.1.6",
44
44
  "@nestjs/passport": "^11.0.5",
45
45
  "@nestjs/terminus": "^11.0.0",
46
46
  "class-transformer": "^0.5.1",
47
47
  "class-validator": "^0.14.2",
48
48
  "express": "^5.1.0",
49
- "firebase-admin": "^13.4.0",
49
+ "firebase-admin": "^13.5.0",
50
50
  "jsonwebtoken": "^9.0.2",
51
51
  "passport-http-bearer": "^1.0.1",
52
- "pino": "^9.7.0",
52
+ "pino": "^9.13.1",
53
53
  "reflect-metadata": "^0.2.2"
54
54
  },
55
55
  "devDependencies": {
56
- "@nestjs/testing": "^11.1.5",
57
- "@swc/core": "^1.13.3",
56
+ "@nestjs/testing": "^11.1.6",
57
+ "@swc/core": "^1.13.5",
58
58
  "@swc/jest": "^0.2.39",
59
59
  "@tsconfig/node22": "^22.0.2",
60
60
  "@types/jest": "^30.0.0",
61
61
  "@types/jsonwebtoken": "^9.0.10",
62
- "@types/node": "^22.17.0",
63
- "@types/passport-http-bearer": "^1.0.41",
62
+ "@types/node": "^22.18.11",
63
+ "@types/passport-http-bearer": "^1.0.42",
64
64
  "@types/supertest": "^6.0.3",
65
- "@types/uuid": "^10.0.0",
66
- "dotenv": "^17.2.1",
67
- "eslint": "^9.32.0",
65
+ "@types/uuid": "^11.0.0",
66
+ "dotenv": "^17.2.3",
67
+ "eslint": "^9.38.0",
68
68
  "eslint-config-prettier": "^10.1.8",
69
- "eslint-plugin-prettier": "^5.5.3",
70
- "jest": "^30.0.5",
69
+ "eslint-plugin-prettier": "^5.5.4",
70
+ "jest": "^30.2.0",
71
71
  "jest-extended": "^6.0.0",
72
- "pino-pretty": "^13.1.1",
72
+ "pino-pretty": "^13.1.2",
73
73
  "rimraf": "^6.0.1",
74
74
  "supertest": "^7.1.4",
75
- "typescript": "^5.9.2",
76
- "typescript-eslint": "^8.39.0",
77
- "uuid": "^11.1.0"
75
+ "typescript": "^5.9.3",
76
+ "typescript-eslint": "^8.46.1",
77
+ "uuid": "^13.0.0"
78
78
  }
79
79
  }