@lokalise/prisma-utils 6.0.1 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,7 +30,7 @@ const prisma = prismaClientFactory(PrismaClient, {
30
30
  ```typescript
31
31
  import { prismaClientFactory } from '@lokalise/prisma-utils'
32
32
  import { PrismaClient } from '@prisma/client'
33
- import * as promClient from 'prom-client'
33
+ import * as promClient from '@prometheus-io/client'
34
34
 
35
35
  const prisma = prismaClientFactory(
36
36
  PrismaClient,
@@ -142,3 +142,63 @@ when multiple transactions compete for the same resources. This helper abstracts
142
142
  errors, providing a robust and efficient way to ensure your transactions succeed even under high contention.
143
143
  By automatically retrying failed transactions with an intelligent backoff strategy, you can significantly improve the
144
144
  reliability and performance of your application when using distributed databases.
145
+
146
+ ### Prisma Bulk Update
147
+
148
+ Updates many rows in a single atomic SQL statement (`UPDATE ... FROM (VALUES ...)`), instead of issuing one `UPDATE` per row. Because it is a single statement, it either fully applies or fully rolls back, so no surrounding transaction is required. Works on both CockroachDB and PostgreSQL.
149
+
150
+ #### Basic Usage
151
+
152
+ ```typescript
153
+ import { prismaBulkUpdate } from '@lokalise/prisma-utils'
154
+
155
+ await prismaBulkUpdate(prisma, 'segment', {
156
+ dbDriver: 'CockroachDb',
157
+ typeByColumn: { id: 'uuid', value: 'text', words_count: 'int4' },
158
+ }, [
159
+ { where: { id: id1 }, data: { value: 'a', words_count: 1 } },
160
+ { where: { id: id2 }, data: { value: 'b', words_count: 2 } },
161
+ ])
162
+ ```
163
+
164
+ The above executes a single statement of the form:
165
+
166
+ ```sql
167
+ UPDATE "segment"
168
+ SET "value" = updates."value"::text, "words_count" = updates."words_count"::int4
169
+ FROM (VALUES ($1::uuid, $2::text, $3::int4), ($4::uuid, $5::text, $6::int4))
170
+ AS updates("id", "value", "words_count")
171
+ WHERE "segment"."id" = updates."id"::uuid
172
+ ```
173
+
174
+ #### Parameters
175
+
176
+ - **`prisma`** — a Prisma client or a transaction client (e.g. the `tx` passed to `prismaTransaction`'s callback).
177
+ - **`tableName`** — target table, optionally schema-qualified (e.g. `'translation.segment'`).
178
+ - **`options`**:
179
+ - **`dbDriver`** — `'CockroachDb'` or `'Postgres'`. Selects which column-type vocabulary `typeByColumn` autocompletes; the emitted SQL is identical for both.
180
+ - **`typeByColumn`** — explicit SQL type for every `where` column and every defined `data` column. The explicit `::type` cast is required by CockroachDB (it will not infer placeholder types inside a `VALUES` list) and is also valid PostgreSQL.
181
+ - **`returning`** (optional) — map of DB column name to the result alias to expose via a `RETURNING` clause.
182
+ - **`entries`** — the rows to update; every entry must specify the same `where` columns and the same set of defined `data` columns.
183
+
184
+ #### Behavior Notes
185
+
186
+ - **`undefined` vs `null`** — following Prisma's convention, an `undefined` value in `data` leaves that column untouched (it is dropped from the statement), while `null` sets the column to SQL `NULL`.
187
+ - **JSON columns** — values for `json`/`jsonb` columns are `JSON.stringify`-ed automatically; everything else is bound natively.
188
+ - **Returning rows** — when `returning` is provided (and non-empty), the updated rows are returned aliased per the map; when it is omitted or an empty map, an empty array is returned. The row type can be set via the generic:
189
+
190
+ ```typescript
191
+ const updated = await prismaBulkUpdate<{ id: string; value: string }>(
192
+ prisma,
193
+ 'segment',
194
+ {
195
+ dbDriver: 'CockroachDb',
196
+ typeByColumn: { id: 'uuid', value: 'text' },
197
+ returning: { id: 'id', value: 'value' },
198
+ },
199
+ [{ where: { id: id1 }, data: { value: 'a' } }],
200
+ )
201
+ ```
202
+
203
+ - **Limits** — at most 1000 entries per call, and the total bound parameters (`entries × (where columns + data columns)`) must not exceed 65535 — the shared protocol limit for PostgreSQL and CockroachDB. Both are validated up front. A column may not appear in both `where` and `data`.
204
+ - **Trusted identifiers** — `tableName`, all column names, the column types, and the `returning` keys/aliases are interpolated as raw SQL identifiers (only row values are bound). They must be static, trusted configuration, never end-user input.
@@ -0,0 +1,2 @@
1
+ export { prismaBulkUpdate } from './prismaBulkUpdate.ts';
2
+ export type * from './types.ts';
@@ -0,0 +1,2 @@
1
+ export { prismaBulkUpdate } from './prismaBulkUpdate.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/bulk-update/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA"}
@@ -0,0 +1,55 @@
1
+ import type { PrismaClient } from 'db-client/client.ts';
2
+ import type { PrismaTransactionClient } from '../transaction/types.ts';
3
+ import type { BulkUpdateEntry, PrismaBulkUpdateOptions } from './types.ts';
4
+ /**
5
+ * Performs a full bulk update operation using Prisma in a single SQL statement.
6
+ * Because it is a single statement, the update is atomic: it either fully applies
7
+ * or fully rolls back, so no surrounding transaction is required. Works on both
8
+ * CockroachDB and PostgreSQL (selected via `options.dbDriver`).
9
+ *
10
+ * The executed query follows the below structure for provided example data:
11
+ * [
12
+ * { where: { id: 1 }, data: { col1: 11, col2: 12 } },
13
+ * { where: { id: 2 }, data: { col1: 21, col2: 22 } },
14
+ * ]
15
+ *
16
+ * UPDATE "tbl"
17
+ * SET "col1" = updates."col1", "col2" = updates."col2"
18
+ * FROM (
19
+ * VALUES
20
+ * (1, 11, 12),
21
+ * (2, 21, 22)
22
+ * ) AS updates("id", "col1", "col2")
23
+ * WHERE "tbl"."id" = updates."id"
24
+ *
25
+ * "data" values follow Prisma's convention: an `undefined` value leaves the
26
+ * column untouched (it is dropped from the statement), while `null` sets it to
27
+ * SQL `NULL`. Every entry must specify the same "where" columns and the same set
28
+ * of defined (non-`undefined`) "data" columns.
29
+ *
30
+ * When `options.returning` is provided, the updated rows are returned via a
31
+ * `RETURNING` clause built from it: each entry maps a DB column name to the alias
32
+ * the row should expose (e.g. `content_unit_id` -> `contentUnitId`). Columns are
33
+ * qualified with the target table (e.g. `"translation"."segment"."value"`) to
34
+ * avoid ambiguity with the `updates` source. Without it (or with an empty map)
35
+ * the statement runs as a plain update and an empty array is returned.
36
+ *
37
+ * Note on identifiers: `tableName`, the column names (`typeByColumn` keys and the
38
+ * `where`/`data` keys), the column `type`s, and the `returning` keys/aliases are
39
+ * interpolated into the SQL as raw identifiers — only the row *values* are bound
40
+ * as parameters. These must therefore be trusted, static configuration, never
41
+ * end-user input: a name containing a `"` would break out of its quoted
42
+ * identifier, and any string assigned to a `(string & {})` column type reaches
43
+ * the SQL verbatim.
44
+ *
45
+ * @template T - The shape of each row returned via `options.returning`.
46
+ * @template P - The concrete Prisma client type, inferred from `prisma` so that both
47
+ * a full client and its derived transaction client (`PrismaTransactionClient<P>`) are accepted.
48
+ * @param prisma - The Prisma client instance (or a transaction client, e.g. inside `prismaTransaction`).
49
+ * @param tableName - The name of the table to update (may be schema-qualified, e.g. `translation.segment`).
50
+ * @param options - `dbDriver`, the `typeByColumn` map (must map every "where" column and every
51
+ * "data" column that has a defined value), and an optional `returning` map.
52
+ * @param entries - The entries containing the match condition and column values for the update.
53
+ * @returns The updated rows (aliased per `options.returning`), or an empty array when it is omitted.
54
+ */
55
+ export declare const prismaBulkUpdate: <T = unknown, P extends PrismaClient = PrismaClient>(prisma: P | PrismaTransactionClient<P>, tableName: string, options: PrismaBulkUpdateOptions, entries: BulkUpdateEntry[]) => Promise<T[]>;
@@ -0,0 +1,195 @@
1
+ import { join, raw, sqltag as sql } from '@prisma/client/runtime/client';
2
+ const JSON_COLUMN_TYPES = new Set(['json', 'jsonb']);
3
+ const ENTRIES_LIMIT = 1000;
4
+ /**
5
+ * Every value (each "where" and each defined "data" cell) becomes one bound
6
+ * placeholder in the `VALUES` list, so the statement uses
7
+ * `entries × (whereColumns + dataColumns)` parameters. PostgreSQL caps a single
8
+ * statement at 65535 bound parameters and CockroachDB has its own ceiling, so we
9
+ * guard the total up front to fail with a clear message instead of an opaque
10
+ * driver error. The effective row cap therefore shrinks as the statement widens.
11
+ */
12
+ const BIND_PARAMETERS_LIMIT = 65535;
13
+ /**
14
+ * Performs a full bulk update operation using Prisma in a single SQL statement.
15
+ * Because it is a single statement, the update is atomic: it either fully applies
16
+ * or fully rolls back, so no surrounding transaction is required. Works on both
17
+ * CockroachDB and PostgreSQL (selected via `options.dbDriver`).
18
+ *
19
+ * The executed query follows the below structure for provided example data:
20
+ * [
21
+ * { where: { id: 1 }, data: { col1: 11, col2: 12 } },
22
+ * { where: { id: 2 }, data: { col1: 21, col2: 22 } },
23
+ * ]
24
+ *
25
+ * UPDATE "tbl"
26
+ * SET "col1" = updates."col1", "col2" = updates."col2"
27
+ * FROM (
28
+ * VALUES
29
+ * (1, 11, 12),
30
+ * (2, 21, 22)
31
+ * ) AS updates("id", "col1", "col2")
32
+ * WHERE "tbl"."id" = updates."id"
33
+ *
34
+ * "data" values follow Prisma's convention: an `undefined` value leaves the
35
+ * column untouched (it is dropped from the statement), while `null` sets it to
36
+ * SQL `NULL`. Every entry must specify the same "where" columns and the same set
37
+ * of defined (non-`undefined`) "data" columns.
38
+ *
39
+ * When `options.returning` is provided, the updated rows are returned via a
40
+ * `RETURNING` clause built from it: each entry maps a DB column name to the alias
41
+ * the row should expose (e.g. `content_unit_id` -> `contentUnitId`). Columns are
42
+ * qualified with the target table (e.g. `"translation"."segment"."value"`) to
43
+ * avoid ambiguity with the `updates` source. Without it (or with an empty map)
44
+ * the statement runs as a plain update and an empty array is returned.
45
+ *
46
+ * Note on identifiers: `tableName`, the column names (`typeByColumn` keys and the
47
+ * `where`/`data` keys), the column `type`s, and the `returning` keys/aliases are
48
+ * interpolated into the SQL as raw identifiers — only the row *values* are bound
49
+ * as parameters. These must therefore be trusted, static configuration, never
50
+ * end-user input: a name containing a `"` would break out of its quoted
51
+ * identifier, and any string assigned to a `(string & {})` column type reaches
52
+ * the SQL verbatim.
53
+ *
54
+ * @template T - The shape of each row returned via `options.returning`.
55
+ * @template P - The concrete Prisma client type, inferred from `prisma` so that both
56
+ * a full client and its derived transaction client (`PrismaTransactionClient<P>`) are accepted.
57
+ * @param prisma - The Prisma client instance (or a transaction client, e.g. inside `prismaTransaction`).
58
+ * @param tableName - The name of the table to update (may be schema-qualified, e.g. `translation.segment`).
59
+ * @param options - `dbDriver`, the `typeByColumn` map (must map every "where" column and every
60
+ * "data" column that has a defined value), and an optional `returning` map.
61
+ * @param entries - The entries containing the match condition and column values for the update.
62
+ * @returns The updated rows (aliased per `options.returning`), or an empty array when it is omitted.
63
+ */
64
+ export const prismaBulkUpdate = (prisma, tableName, options, entries) => {
65
+ const { typeByColumn, returning } = options;
66
+ const [firstEntry] = entries;
67
+ if (!firstEntry) {
68
+ throw new Error('Entries array must not be empty');
69
+ }
70
+ if (entries.length > ENTRIES_LIMIT) {
71
+ throw new Error(`Entries array length must not exceed ${ENTRIES_LIMIT}`);
72
+ }
73
+ const whereColumns = resolveColumns(typeByColumn, Object.keys(firstEntry.where));
74
+ const dataColumns = resolveColumns(typeByColumn, definedColumnNames(firstEntry.data));
75
+ if (whereColumns.length === 0) {
76
+ throw new Error('Entry "where" object must not be empty');
77
+ }
78
+ if (dataColumns.length === 0) {
79
+ throw new Error('Entry "data" object must not be empty');
80
+ }
81
+ const undefinedWhereColumn = Object.keys(firstEntry.where).find((columnName) => firstEntry.where[columnName] === undefined);
82
+ if (undefinedWhereColumn) {
83
+ throw new Error(`Entry "where" column "${undefinedWhereColumn}" must not be undefined`);
84
+ }
85
+ // A column appearing in both "where" and "data" would be emitted twice in the
86
+ // `updates(...)` alias list, producing an invalid derived-table column list.
87
+ const overlappingColumn = dataColumns.find((dataColumn) => whereColumns.some((whereColumn) => whereColumn.name === dataColumn.name));
88
+ if (overlappingColumn) {
89
+ throw new Error(`Column "${overlappingColumn.name}" must not appear in both "where" and "data"`);
90
+ }
91
+ const bindParametersCount = entries.length * (whereColumns.length + dataColumns.length);
92
+ if (bindParametersCount > BIND_PARAMETERS_LIMIT) {
93
+ throw new Error(`Bulk update would use ${bindParametersCount} bind parameters ` +
94
+ `(${entries.length} entries × ${whereColumns.length + dataColumns.length} columns), ` +
95
+ `exceeding the limit of ${BIND_PARAMETERS_LIMIT}`);
96
+ }
97
+ // Quote each part of a (possibly schema-qualified) table name separately,
98
+ // so that "translation.segment" becomes "translation"."segment" rather than a
99
+ // single identifier named "translation.segment".
100
+ const quotedTableName = tableName
101
+ .split('.')
102
+ .map((part) => `"${part}"`)
103
+ .join('.');
104
+ const sqlSetExpressions = dataColumns.map((column) => {
105
+ return sql([`"${column.name}" = updates."${column.name}"::${column.type}`]);
106
+ });
107
+ const sqlValuesExpressions = entries.map((entry, index) => prepareSqlValuesExpression(entry, index, whereColumns, dataColumns));
108
+ const sqlValuesColumnAliases = [...whereColumns, ...dataColumns].map((column) => {
109
+ return sql([`"${column.name}"`]);
110
+ });
111
+ const sqlWhereConditions = whereColumns.map((column) => {
112
+ return sql([`${quotedTableName}."${column.name}" = updates."${column.name}"::${column.type}`]);
113
+ });
114
+ const updateStatement = sql `
115
+ UPDATE ${sql([quotedTableName])}
116
+ SET ${join(sqlSetExpressions, ', ')}
117
+ FROM (
118
+ VALUES ${join(sqlValuesExpressions, ',\n')}
119
+ ) AS updates(${join(sqlValuesColumnAliases, ', ')})
120
+ WHERE ${join(sqlWhereConditions, ' AND ')}
121
+ `;
122
+ if (!returning || Object.keys(returning).length === 0) {
123
+ return prisma.$executeRaw(updateStatement).then(() => []);
124
+ }
125
+ const sqlReturningExpressions = Object.entries(returning).map(([column, alias]) => {
126
+ return sql([`${quotedTableName}."${column}" AS "${alias}"`]);
127
+ });
128
+ return prisma.$queryRaw(sql `${updateStatement} RETURNING ${join(sqlReturningExpressions, ', ')}`);
129
+ };
130
+ /**
131
+ * Builds the parenthesized `(where..., set...)` tuple for a single entry — one
132
+ * row of the `VALUES` source. The entry must supply exactly the given `where`
133
+ * columns and the same set of defined `data` columns (the shape established by
134
+ * the first entry); a mismatch throws so the generated `VALUES` list stays
135
+ * rectangular and aligned with the column aliases.
136
+ */
137
+ const prepareSqlValuesExpression = (entry, index, whereColumns, dataColumns) => {
138
+ if (whereColumns.length !== Object.keys(entry.where).length) {
139
+ throw new Error(`Entry "where" columns are not the same (at index ${index})`);
140
+ }
141
+ const sqlWhereValues = whereColumns.map((whereConditionColumn) => {
142
+ const whereConditionValue = entry.where[whereConditionColumn.name];
143
+ if (whereConditionValue === undefined) {
144
+ throw new Error(`Entry "where" column "${whereConditionColumn.name}" was not found (at index ${index})`);
145
+ }
146
+ return renderTypedValue(whereConditionValue, whereConditionColumn.type);
147
+ });
148
+ if (dataColumns.length !== definedColumnNames(entry.data).length) {
149
+ throw new Error(`Entry "data" columns are not the same (at index ${index})`);
150
+ }
151
+ const sqlSetValues = dataColumns.map((setExpressionColumn) => {
152
+ const setExpressionValue = entry.data[setExpressionColumn.name];
153
+ if (setExpressionValue === undefined) {
154
+ throw new Error(`Entry "data" column "${setExpressionColumn.name}" was not found (at index ${index})`);
155
+ }
156
+ return renderTypedValue(setExpressionValue, setExpressionColumn.type);
157
+ });
158
+ return sql `(${join(sqlWhereValues, ',')},${join(sqlSetValues, ',')})`;
159
+ };
160
+ /**
161
+ * Pairs each requested column name with its SQL type from `typeByColumn`,
162
+ * throwing when a column has no type mapping (it would otherwise produce an
163
+ * uncasted, ambiguous placeholder).
164
+ */
165
+ const resolveColumns = (typeByColumn, columnNames) => columnNames.map((columnName) => {
166
+ const type = typeByColumn[columnName];
167
+ if (!type) {
168
+ throw new Error(`Column type mapping is missing for "${columnName}"`);
169
+ }
170
+ return { name: columnName, type };
171
+ });
172
+ /**
173
+ * `data` follows Prisma's convention: an `undefined` value means "leave the
174
+ * column untouched", so such keys are dropped from the statement (a `null` value
175
+ * is kept and becomes a SQL `NULL`). `where` columns are never dropped — they form
176
+ * the match condition.
177
+ */
178
+ const definedColumnNames = (columnRecord) => Object.keys(columnRecord).filter((columnName) => columnRecord[columnName] !== undefined);
179
+ /**
180
+ * Renders a single value as a typed SQL placeholder (e.g. `$1::uuid`).
181
+ *
182
+ * The explicit `::type` cast is required by CockroachDB, which (unlike
183
+ * PostgreSQL) refuses to infer the data type of an untyped placeholder inside a
184
+ * `VALUES` list ("could not determine data type of placeholder"). The cast is
185
+ * also valid PostgreSQL, so the same statement runs on both drivers.
186
+ *
187
+ * Whether a value is JSON-serialized is decided by the column type, not the
188
+ * runtime value: `json`/`jsonb` columns get the value stringified, everything
189
+ * else is bound directly (Prisma binds `Date`, numbers, etc. natively). A `null`
190
+ * is always bound directly so it becomes a SQL `NULL` rather than a JSON `null`.
191
+ */
192
+ const renderTypedValue = (value, type) => value !== null && JSON_COLUMN_TYPES.has(type)
193
+ ? sql `${JSON.stringify(value)}::${raw(type)}`
194
+ : sql `${value}::${raw(type)}`;
195
+ //# sourceMappingURL=prismaBulkUpdate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prismaBulkUpdate.js","sourceRoot":"","sources":["../../src/bulk-update/prismaBulkUpdate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import type { DbDriverEnum } from '../types.ts';
2
+ /**
3
+ * One row of a bulk update: `where` identifies the row(s) to match and `data`
4
+ * holds the new column values. An `undefined` value in `data` follows Prisma's
5
+ * convention and leaves that column untouched; `null` sets a SQL NULL.
6
+ */
7
+ export type BulkUpdateEntry = {
8
+ where: Record<string, unknown>;
9
+ data: Record<string, unknown>;
10
+ };
11
+ /**
12
+ * SQL type names shared by CockroachDB and PostgreSQL, used to render the
13
+ * explicit `::type` cast on every bound value.
14
+ */
15
+ type CommonColumnSqlType = 'uuid' | 'text' | 'bool' | 'int2' | 'int4' | 'int8' | 'float4' | 'float8' | 'numeric' | 'date' | 'timestamptz' | 'json' | 'jsonb';
16
+ /**
17
+ * The literal union provides autocomplete for the common types, while the
18
+ * `(string & {})` member keeps any other type assignable — e.g. schema-qualified
19
+ * enums like `translation.segment_status`.
20
+ */
21
+ export type CockroachDbColumnSqlType = CommonColumnSqlType | 'bytes' | (string & {});
22
+ export type PostgresColumnSqlType = CommonColumnSqlType | 'bytea' | 'varchar' | (string & {});
23
+ /**
24
+ * Options for `prismaBulkUpdate`. `dbDriver` selects which column-type
25
+ * vocabulary `typeByColumn` autocompletes; the emitted SQL is identical for both.
26
+ * `typeByColumn` must map every `where` column and every defined `data` column.
27
+ */
28
+ export type PrismaBulkUpdateOptions = {
29
+ /**
30
+ * Optional map of DB column name to the result alias to expose via a
31
+ * `RETURNING` clause. When omitted, no rows are returned.
32
+ */
33
+ returning?: Record<string, string>;
34
+ } & ({
35
+ dbDriver: typeof DbDriverEnum.COCKROACH_DB;
36
+ typeByColumn: Record<string, CockroachDbColumnSqlType>;
37
+ } | {
38
+ dbDriver: typeof DbDriverEnum.POSTGRES;
39
+ typeByColumn: Record<string, PostgresColumnSqlType>;
40
+ });
41
+ export {};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/bulk-update/types.ts"],"names":[],"mappings":""}
@@ -1,2 +1,2 @@
1
- export * from "./prismaError.js";
1
+ export * from './prismaError.js';
2
2
  //# sourceMappingURL=index.js.map
@@ -1,3 +1,3 @@
1
+ import type Prometheus from '@prometheus-io/client';
1
2
  import type { PrismaClient } from 'db-client/client.ts';
2
- import type Prometheus from 'prom-client';
3
3
  export declare const extendPrismaClientWithMetrics: <Client extends PrismaClient>(prisma: Client, promClient: typeof Prometheus) => Client;
@@ -1,2 +1,2 @@
1
- export { prismaClientFactory } from "./prismaClientFactory.js";
1
+ export { prismaClientFactory } from './prismaClientFactory.js';
2
2
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  import type * as RuntimePrisma from '@prisma/client/runtime/client';
2
+ import type Prometheus from '@prometheus-io/client';
2
3
  import type { PrismaClient } from 'db-client/client.ts';
3
4
  import type { PrismaClientConstructor } from 'db-client/internal/class.ts';
4
- import type Prometheus from 'prom-client';
5
5
  export type prismaClientFactoryOptions = {
6
6
  promClient?: typeof Prometheus;
7
7
  };
@@ -1,4 +1,4 @@
1
- import { extendPrismaClientWithMetrics } from "./extendPrismaClientWithMetrics.js";
1
+ import { extendPrismaClientWithMetrics } from './extendPrismaClientWithMetrics.js';
2
2
  /**
3
3
  * Factory function to create a Prisma client instance with default configuration
4
4
  * and optional Prometheus metrics integration.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './bulk-update/index.ts';
1
2
  export * from './errors/index.ts';
2
3
  export * from './factory/index.ts';
3
4
  export * from './transaction/index.ts';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- export * from "./errors/index.js";
2
- export * from "./factory/index.js";
3
- export * from "./transaction/index.js";
1
+ export * from './bulk-update/index.js';
2
+ export * from './errors/index.js';
3
+ export * from './factory/index.js';
4
+ export * from './transaction/index.js';
4
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,wBAAwB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAA;AACtC,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,wBAAwB,CAAA"}
@@ -1,2 +1,2 @@
1
- export { prismaTransaction } from "./prismaTransaction.js";
1
+ export { prismaTransaction } from './prismaTransaction.js';
2
2
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  import { setTimeout } from 'node:timers/promises';
2
2
  import { deepClone } from '@lokalise/node-core';
3
- import { isCockroachDBRetryTransaction } from "../errors/cockroachdbError.js";
4
- import { isPrismaClientKnownRequestError, isPrismaTransactionClosedError, PRISMA_SERIALIZATION_ERROR, PRISMA_SERVER_CLOSED_CONNECTION_ERROR, PRISMA_TRANSACTION_ERROR, } from "../errors/index.js";
3
+ import { isCockroachDBRetryTransaction } from '../errors/cockroachdbError.js';
4
+ import { isPrismaClientKnownRequestError, isPrismaTransactionClosedError, PRISMA_SERIALIZATION_ERROR, PRISMA_SERVER_CLOSED_CONNECTION_ERROR, PRISMA_TRANSACTION_ERROR, } from '../errors/index.js';
5
5
  const DEFAULT_OPTIONS = {
6
6
  retriesAllowed: 2, // first try + 2 retries = 3 tries
7
7
  baseRetryDelayMs: 100,
package/dist/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  type ObjectValues<T> = T[keyof T];
2
2
  export declare const DbDriverEnum: {
3
- readonly COCKROACH_DB: "CockroachDb";
4
- readonly POSTGRES: "Postgres";
3
+ readonly COCKROACH_DB: 'CockroachDb';
4
+ readonly POSTGRES: 'Postgres';
5
5
  };
6
6
  export type DbDriver = ObjectValues<typeof DbDriverEnum>;
7
7
  export {};
package/package.json CHANGED
@@ -1,56 +1,57 @@
1
1
  {
2
- "name": "@lokalise/prisma-utils",
3
- "version": "6.0.1",
4
- "type": "module",
5
- "license": "Apache-2.0",
6
- "files": [
7
- "dist",
8
- "README.md",
9
- "LICENSE.md"
10
- ],
11
- "main": "./dist/index.js",
12
- "exports": {
13
- ".": "./dist/index.js",
14
- "./package.json": "./package.json"
15
- },
16
- "homepage": "https://github.com/lokalise/shared-ts-libs",
17
- "repository": {
18
- "type": "git",
19
- "url": "https://github.com/lokalise/shared-ts-libs.git"
20
- },
21
- "scripts": {
22
- "build": "rimraf dist && npm run db:update-client && tsc --project tsconfig.build.json",
23
- "clean": "rimraf dist",
24
- "lint": "biome check . && tsc",
25
- "lint:fix": "biome check --write",
26
- "db:migration:dev": "prisma migrate dev --config=./prisma/prisma.config",
27
- "db:update-client": "prisma generate",
28
- "test": "vitest run",
29
- "test:migrate": "cross-env NODE_ENV=test prisma migrate reset --config=./prisma/prisma.config --force",
30
- "pretest:ci": "docker compose up -d --wait && npm run test:migrate",
31
- "test:ci": "npm run test",
32
- "posttest:ci": "docker compose down",
33
- "prepublishOnly": "npm run build",
34
- "package-version": "echo $npm_package_version",
35
- "postversion": "biome check --write package.json"
36
- },
37
- "peerDependencies": {
38
- "@lokalise/node-core": ">=14.7.4",
39
- "@prisma/client": "^7.0.0",
40
- "prisma": "^7.0.0",
41
- "prom-client": ">=15.0.0"
42
- },
43
- "devDependencies": {
44
- "@biomejs/biome": "^2.3.7",
45
- "@lokalise/biome-config": "^3.1.0",
46
- "@lokalise/tsconfig": "^3.1.0",
47
- "@prisma/adapter-pg": "^7.4.0",
48
- "@prisma/client": "^7.4.0",
49
- "@vitest/coverage-v8": "^4.0.15",
50
- "cross-env": "^10.0.0",
51
- "prisma": "^7.4.0",
52
- "rimraf": "^6.0.1",
53
- "typescript": "5.9.3",
54
- "vitest": "^4.0.15"
55
- }
56
- }
2
+ "name": "@lokalise/prisma-utils",
3
+ "version": "7.0.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "files": [
7
+ "dist",
8
+ "README.md",
9
+ "LICENSE.md"
10
+ ],
11
+ "main": "./dist/index.js",
12
+ "exports": {
13
+ ".": "./dist/index.js",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "homepage": "https://github.com/lokalise/shared-ts-libs",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/lokalise/shared-ts-libs.git",
20
+ "directory": "packages/app/prisma-utils"
21
+ },
22
+ "peerDependencies": {
23
+ "@lokalise/node-core": ">=14.7.4",
24
+ "@prisma/client": "^7.0.0",
25
+ "@prometheus-io/client": "^0.16.1",
26
+ "prisma": "^7.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@biomejs/biome": "^2.5.9",
30
+ "dotenv": "^17.0.0",
31
+ "@prisma/adapter-pg": "^7.4.0",
32
+ "@prisma/client": "^7.4.0",
33
+ "@vitest/coverage-v8": "^4.1.11",
34
+ "cross-env": "^10.0.0",
35
+ "prisma": "^7.10.0",
36
+ "rimraf": "^6.1.3",
37
+ "typescript": "7.0.2",
38
+ "vitest": "^4.1.11",
39
+ "@lokalise/biome-config": "^3.1.1",
40
+ "@lokalise/tsconfig": "^5.0.0"
41
+ },
42
+ "scripts": {
43
+ "build": "rimraf dist && pnpm run db:update-client && tsc --project tsconfig.build.json",
44
+ "clean": "rimraf dist",
45
+ "lint": "biome check . && tsc",
46
+ "lint:fix": "biome check --write",
47
+ "db:migration:dev": "prisma migrate dev --config=./prisma/prisma.config",
48
+ "db:update-client": "prisma generate",
49
+ "test": "vitest run",
50
+ "test:migrate": "cross-env NODE_ENV=test prisma migrate reset --config=./prisma/prisma.config --force",
51
+ "pretest:ci": "docker compose up -d --wait && pnpm run test:migrate",
52
+ "test:ci": "pnpm run test",
53
+ "posttest:ci": "docker compose down",
54
+ "package-version": "echo $npm_package_version",
55
+ "postversion": "biome check --write package.json"
56
+ }
57
+ }