@segmentstream-cloud/ontology 0.25.1 → 0.26.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 +78 -0
- package/dist/entity.d.ts +53 -2
- package/dist/entity.js +31 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +8 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/runtime/batch-diagnostics.d.ts +22 -0
- package/dist/runtime/batch-diagnostics.js +89 -0
- package/dist/runtime/batch.d.ts +48 -0
- package/dist/runtime/batch.js +473 -0
- package/dist/runtime/records.d.ts +2 -2
- package/dist/runtime/records.js +25 -6
- package/dist/runtime/sql-runtime.js +125 -5
- package/dist/runtime.d.ts +51 -2
- package/dist/runtime.js +2 -1
- package/dist/service/errors.js +17 -0
- package/dist/service/schemas.js +4 -0
- package/package.json +4 -2
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Effect, Option } from "effect";
|
|
1
|
+
import { Effect, Exit, Option } from "effect";
|
|
2
2
|
import { beginReplicationWrite } from "../replication/transaction.js";
|
|
3
3
|
import { isSqlError } from "effect/unstable/sql/SqlError";
|
|
4
|
-
import { Conflict } from "../errors.js";
|
|
4
|
+
import { ActionRejected, Conflict } from "../errors.js";
|
|
5
5
|
import { toSnakeCase } from "../names.js";
|
|
6
6
|
import { User } from "../user.js";
|
|
7
|
+
import { ACTION_MAX_BATCH_ROWS, BATCH_MAX_BYTES, BATCH_MAX_ROWS, batchInputBytes, executeBatch, planBatch, } from "./batch.js";
|
|
8
|
+
import { logBatchConflict } from "./batch-diagnostics.js";
|
|
7
9
|
import { InvalidQuery, WriteOutsideAction } from "./errors.js";
|
|
8
10
|
import { entityModel, ontologyModel } from "./model.js";
|
|
9
11
|
import { loadRecord, notFound, readHistory, relatedCondition, runQuery, validateQuery, } from "./reads.js";
|
|
@@ -23,7 +25,7 @@ function withoutUndefined(value) {
|
|
|
23
25
|
.map(([key, entry]) => [key, withoutUndefined(entry)]));
|
|
24
26
|
}
|
|
25
27
|
/** A unique violation becomes Conflict, naming the property when the index covers one column. */
|
|
26
|
-
function uniqueConflict(entity, error) {
|
|
28
|
+
function uniqueConflict(entity, error, batch) {
|
|
27
29
|
const cause = (error.reason.cause ?? {});
|
|
28
30
|
const unique = error.reason._tag === "UniqueViolation" || cause.code === "23505";
|
|
29
31
|
if (!unique) {
|
|
@@ -31,17 +33,36 @@ function uniqueConflict(entity, error) {
|
|
|
31
33
|
}
|
|
32
34
|
const columns = typeof cause.detail === "string" ? /^Key \(([^)]*)\)=/.exec(cause.detail)?.[1] : undefined;
|
|
33
35
|
const field = entity.fields.find((candidate) => candidate.column === columns);
|
|
36
|
+
const fields = typeof columns === "string"
|
|
37
|
+
? columns.split(", ").flatMap((column) => {
|
|
38
|
+
const candidate = entity.fields.find((entry) => entry.column === column);
|
|
39
|
+
return candidate === undefined ? [] : [candidate.relationship ?? candidate.name];
|
|
40
|
+
})
|
|
41
|
+
: [];
|
|
42
|
+
const details = batch ? { batch: { indices: [], ...(fields.length > 0 ? { fields } : {}) } } : {};
|
|
34
43
|
const label = entity.label.toLowerCase();
|
|
35
|
-
|
|
44
|
+
const conflict = field === undefined
|
|
36
45
|
? new Conflict({
|
|
37
46
|
entity: entity.name,
|
|
38
47
|
message: `The ${label} conflicts with an existing one.`,
|
|
48
|
+
...details,
|
|
39
49
|
})
|
|
40
50
|
: new Conflict({
|
|
41
51
|
entity: entity.name,
|
|
42
52
|
property: field.name,
|
|
43
53
|
message: `A ${label} with this ${field.name} already exists.`,
|
|
44
|
-
|
|
54
|
+
...details,
|
|
55
|
+
});
|
|
56
|
+
if (batch) {
|
|
57
|
+
// Keep the driver's complete cause available to operators without serializing SQL or values.
|
|
58
|
+
Object.defineProperty(conflict, "cause", { value: error, enumerable: false });
|
|
59
|
+
return logBatchConflict(entity, error, {
|
|
60
|
+
operation: batch.operation,
|
|
61
|
+
action: batch.scope.action,
|
|
62
|
+
actionId: batch.scope.actionId,
|
|
63
|
+
}).pipe(Effect.andThen(Effect.fail(conflict)));
|
|
64
|
+
}
|
|
65
|
+
return Effect.fail(conflict);
|
|
45
66
|
}
|
|
46
67
|
/**
|
|
47
68
|
* Platform identity bookkeeping touches only User properties and their history. User is excluded
|
|
@@ -91,6 +112,8 @@ export function makeOntologyRuntime(ontology, sql, scope) {
|
|
|
91
112
|
run: (text, params) => client.unsafe(text, params.values),
|
|
92
113
|
};
|
|
93
114
|
const entityOf = (name) => Effect.sync(() => entityModel(model, name));
|
|
115
|
+
let attemptedBatchRows = 0;
|
|
116
|
+
let attributionInitialized = false;
|
|
94
117
|
const write = (entityName, operation) => Effect.gen(function* () {
|
|
95
118
|
const entity = yield* entityOf(entityName);
|
|
96
119
|
if (scope === undefined) {
|
|
@@ -108,14 +131,111 @@ export function makeOntologyRuntime(ontology, sql, scope) {
|
|
|
108
131
|
for (const edit of edits) {
|
|
109
132
|
scope.onEdit?.(edit);
|
|
110
133
|
}
|
|
134
|
+
if (edits.length > 0 && scope.execution !== undefined) {
|
|
135
|
+
attributionInitialized = true;
|
|
136
|
+
}
|
|
111
137
|
return result;
|
|
112
138
|
});
|
|
139
|
+
const batch = (entityName, operation, input) => Effect.suspend(() => {
|
|
140
|
+
const startedAt = Date.now();
|
|
141
|
+
let statements = 0;
|
|
142
|
+
let distinctReferences = 0;
|
|
143
|
+
let counts = { created: 0, updated: 0, unchanged: 0 };
|
|
144
|
+
const run = Effect.gen(function* () {
|
|
145
|
+
if (scope === undefined) {
|
|
146
|
+
return yield* Effect.die(new WriteOutsideAction({
|
|
147
|
+
entity: entityName,
|
|
148
|
+
message: `Writes to ${entityName} run inside an action, which history records.`,
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
const declared = ontology.entities.find((entity) => entity.name === entityName);
|
|
152
|
+
if (declared?.builtIn === true || declared?.entity.definition.source !== undefined) {
|
|
153
|
+
return yield* new ActionRejected({
|
|
154
|
+
message: "Batch writes require an ordinary stored entity.",
|
|
155
|
+
batch: { indices: [] },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const entity = yield* entityOf(entityName);
|
|
159
|
+
const length = input.records.length;
|
|
160
|
+
if (length > 0) {
|
|
161
|
+
attemptedBatchRows += length;
|
|
162
|
+
}
|
|
163
|
+
if (length > BATCH_MAX_ROWS || (length > 0 && attemptedBatchRows > ACTION_MAX_BATCH_ROWS)) {
|
|
164
|
+
return yield* new ActionRejected({
|
|
165
|
+
message: length > BATCH_MAX_ROWS
|
|
166
|
+
? `A batch accepts at most ${BATCH_MAX_ROWS} records.`
|
|
167
|
+
: `An action accepts at most ${ACTION_MAX_BATCH_ROWS} attempted batch records.`,
|
|
168
|
+
batch: { indices: [] },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (batchInputBytes(input) > BATCH_MAX_BYTES) {
|
|
172
|
+
return yield* new ActionRejected({
|
|
173
|
+
message: `A batch accepts at most ${BATCH_MAX_BYTES} UTF-8 JSON bytes.`,
|
|
174
|
+
batch: { indices: [] },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const plan = yield* planBatch(entity, operation, input);
|
|
178
|
+
distinctReferences = plan.distinctReferences;
|
|
179
|
+
if (length === 0) {
|
|
180
|
+
return plan.returning === "summary" ? counts : { ...counts, items: [] };
|
|
181
|
+
}
|
|
182
|
+
const enclosingTransaction = yield* Effect.serviceOption(sql.transactionService);
|
|
183
|
+
if (Option.isNone(enclosingTransaction)) {
|
|
184
|
+
return yield* Effect.die(new WriteOutsideAction({
|
|
185
|
+
entity: entityName,
|
|
186
|
+
message: "Batch writes require the enclosing action transaction.",
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
const edits = [];
|
|
190
|
+
statements += 1; // The SQL client's nested transaction issues one savepoint.
|
|
191
|
+
const result = yield* sql
|
|
192
|
+
.withTransaction((scope.replicationInitialized ? Effect.void : beginReplicationWrite(sql)).pipe(Effect.andThen(executeBatch({
|
|
193
|
+
...context,
|
|
194
|
+
scope,
|
|
195
|
+
edits,
|
|
196
|
+
attributionInitialized,
|
|
197
|
+
run: (text, params) => {
|
|
198
|
+
statements += 1;
|
|
199
|
+
return context.run(text, params);
|
|
200
|
+
},
|
|
201
|
+
}, entity, plan)), Effect.onExit((exit) => Effect.sync(() => {
|
|
202
|
+
if (Exit.isFailure(exit)) {
|
|
203
|
+
statements += 1; // A failed nested transaction rolls back to its savepoint.
|
|
204
|
+
}
|
|
205
|
+
}))))
|
|
206
|
+
.pipe(Effect.catchIf(isSqlError, (error) => uniqueConflict(entity, error, { operation, scope })));
|
|
207
|
+
counts = { created: result.created, updated: result.updated, unchanged: result.unchanged };
|
|
208
|
+
for (const edit of edits) {
|
|
209
|
+
scope.onEdit?.(edit);
|
|
210
|
+
}
|
|
211
|
+
if (edits.length > 0 && scope.execution !== undefined) {
|
|
212
|
+
attributionInitialized = true;
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
});
|
|
216
|
+
return run.pipe(Effect.onExit((exit) => Effect.logDebug("ontology.action.batch_completed").pipe(Effect.annotateLogs({
|
|
217
|
+
operation,
|
|
218
|
+
entity: entityName,
|
|
219
|
+
action: scope?.action,
|
|
220
|
+
actionId: scope?.actionId,
|
|
221
|
+
inputCount: input.records.length,
|
|
222
|
+
distinctReferenceCount: distinctReferences,
|
|
223
|
+
changedCount: counts.created + counts.updated,
|
|
224
|
+
unchangedCount: counts.unchanged,
|
|
225
|
+
sqlStatements: statements,
|
|
226
|
+
batchMs: Date.now() - startedAt,
|
|
227
|
+
outcome: Exit.isSuccess(exit) ? "success" : "failure",
|
|
228
|
+
}))));
|
|
229
|
+
});
|
|
113
230
|
const query = (input) => Effect.gen(function* () {
|
|
114
231
|
const entity = yield* entityOf(input.entity);
|
|
115
232
|
const validated = yield* validateQuery(entity, input.query);
|
|
116
233
|
return yield* orDieOnSql(runQuery(context, entity, validated));
|
|
117
234
|
});
|
|
118
235
|
return {
|
|
236
|
+
createMany: ((input) => batch(input.entity, "create", input)),
|
|
237
|
+
updateMany: ((input) => batch(input.entity, "update", input)),
|
|
238
|
+
upsertMany: ((input) => batch(input.entity, "upsert", input)),
|
|
119
239
|
get: (input) => orDieOnSql(Effect.gen(function* () {
|
|
120
240
|
const entity = yield* entityOf(input.entity);
|
|
121
241
|
const record = yield* loadRecord(context, entity, input.id, input.forUpdate === true ? "update" : undefined);
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Context, Effect } from "effect";
|
|
2
|
-
import type { Conflict, NotFound } from "./errors.js";
|
|
2
|
+
import type { ActionRejected, Conflict, NotFound } from "./errors.js";
|
|
3
3
|
import type { Where } from "./query.js";
|
|
4
4
|
import type { WorkloadExecution } from "./runtime/workload.js";
|
|
5
5
|
/** Who runs an action: an app member or a service principal, both `user` records. */
|
|
@@ -76,6 +76,51 @@ export interface UpsertResult<R = StoredRecord> {
|
|
|
76
76
|
readonly record: R;
|
|
77
77
|
readonly created: boolean;
|
|
78
78
|
}
|
|
79
|
+
/** Whether a batch returns its records or only outcome counts. Defaults to `records`. */
|
|
80
|
+
export type BatchReturning = "records" | "summary";
|
|
81
|
+
export interface BatchOptions {
|
|
82
|
+
readonly returning?: BatchReturning;
|
|
83
|
+
}
|
|
84
|
+
/** Outcome counts, which together equal the number of inputs in a successful batch. */
|
|
85
|
+
export interface BatchSummaryResult {
|
|
86
|
+
readonly created: number;
|
|
87
|
+
readonly updated: number;
|
|
88
|
+
readonly unchanged: number;
|
|
89
|
+
}
|
|
90
|
+
/** One result per input, in input order, including records whose values did not change. */
|
|
91
|
+
export interface BatchRecordResult<R = StoredRecord> extends BatchSummaryResult {
|
|
92
|
+
readonly items: ReadonlyArray<{
|
|
93
|
+
readonly index: number;
|
|
94
|
+
readonly status: "created" | "updated" | "unchanged";
|
|
95
|
+
readonly record: R;
|
|
96
|
+
}>;
|
|
97
|
+
}
|
|
98
|
+
export type BatchResult<R = StoredRecord> = BatchRecordResult<R> | BatchSummaryResult;
|
|
99
|
+
/** Raw batch inputs use wire entity and field names. */
|
|
100
|
+
export interface CreateManyInput extends BatchOptions {
|
|
101
|
+
readonly entity: string;
|
|
102
|
+
readonly records: ReadonlyArray<StoredValues>;
|
|
103
|
+
}
|
|
104
|
+
export interface UpdateManyInput extends BatchOptions {
|
|
105
|
+
readonly entity: string;
|
|
106
|
+
readonly records: ReadonlyArray<{
|
|
107
|
+
readonly id: string;
|
|
108
|
+
readonly values: StoredValues;
|
|
109
|
+
}>;
|
|
110
|
+
}
|
|
111
|
+
export interface UpsertManyInput extends CreateManyInput {
|
|
112
|
+
readonly on: readonly [string, ...string[]];
|
|
113
|
+
}
|
|
114
|
+
type BatchWriteError = ActionRejected | Conflict | NotFound;
|
|
115
|
+
interface BatchWrite<Input extends BatchOptions> {
|
|
116
|
+
(input: Input & {
|
|
117
|
+
readonly returning: "summary";
|
|
118
|
+
}): Effect.Effect<BatchSummaryResult, BatchWriteError>;
|
|
119
|
+
(input: Input & {
|
|
120
|
+
readonly returning?: "records";
|
|
121
|
+
}): Effect.Effect<BatchRecordResult, BatchWriteError>;
|
|
122
|
+
(input: Input): Effect.Effect<BatchResult, BatchWriteError>;
|
|
123
|
+
}
|
|
79
124
|
type ReadsService = {
|
|
80
125
|
readonly get: (input: {
|
|
81
126
|
readonly entity: string;
|
|
@@ -88,6 +133,9 @@ type ReadsService = {
|
|
|
88
133
|
}) => Effect.Effect<ReadonlyArray<StoredRecord>>;
|
|
89
134
|
};
|
|
90
135
|
type WritesService = {
|
|
136
|
+
readonly createMany: BatchWrite<CreateManyInput>;
|
|
137
|
+
readonly updateMany: BatchWrite<UpdateManyInput>;
|
|
138
|
+
readonly upsertMany: BatchWrite<UpsertManyInput>;
|
|
91
139
|
readonly create: (input: {
|
|
92
140
|
readonly entity: string;
|
|
93
141
|
readonly values: StoredValues;
|
|
@@ -130,7 +178,8 @@ export declare class OntologyReads extends OntologyReads_base {
|
|
|
130
178
|
declare const OntologyWrites_base: Context.ServiceClass<OntologyWrites, "@segmentstream-cloud/ontology/OntologyWrites", WritesService>;
|
|
131
179
|
/**
|
|
132
180
|
* The writes an action handler runs, inside the action's transaction: `create`, `update`,
|
|
133
|
-
* `upsertBy`, `delete`, `link` and `unlink` of entity handles require it.
|
|
181
|
+
* `upsertBy`, the batch methods, `delete`, `link` and `unlink` of entity handles require it.
|
|
182
|
+
* `prepare` never has it.
|
|
134
183
|
*/
|
|
135
184
|
export declare class OntologyWrites extends OntologyWrites_base {
|
|
136
185
|
}
|
package/dist/runtime.js
CHANGED
|
@@ -8,7 +8,8 @@ export class OntologyReads extends Context.Service()("@segmentstream-cloud/ontol
|
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* The writes an action handler runs, inside the action's transaction: `create`, `update`,
|
|
11
|
-
* `upsertBy`, `delete`, `link` and `unlink` of entity handles require it.
|
|
11
|
+
* `upsertBy`, the batch methods, `delete`, `link` and `unlink` of entity handles require it.
|
|
12
|
+
* `prepare` never has it.
|
|
12
13
|
*/
|
|
13
14
|
export class OntologyWrites extends Context.Service()("@segmentstream-cloud/ontology/OntologyWrites") {
|
|
14
15
|
}
|
package/dist/service/errors.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Cause, Context, Effect, Option, Predicate, Schema, SchemaIssue } from "effect";
|
|
2
2
|
import { HttpServerResponse } from "effect/unstable/http";
|
|
3
3
|
import { HttpApiError } from "effect/unstable/httpapi";
|
|
4
|
+
import { BatchErrorDetails } from "../errors.js";
|
|
4
5
|
/** The correlation id of the current request, in every error body and the `x-request-id` header. */
|
|
5
6
|
export class RequestId extends Context.Service()("@segmentstream-cloud/ontology/RequestId") {
|
|
6
7
|
}
|
|
@@ -64,6 +65,19 @@ function listed(values) {
|
|
|
64
65
|
? values.join("")
|
|
65
66
|
: `${values.slice(0, -1).join(", ")} and ${values.at(-1)}`;
|
|
66
67
|
}
|
|
68
|
+
/** Only the declared safe locations cross a transport, even for structurally tagged failures. */
|
|
69
|
+
function batchDetails(error) {
|
|
70
|
+
const batch = error.batch;
|
|
71
|
+
if (!Schema.is(BatchErrorDetails)(batch)) {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
batch: {
|
|
76
|
+
indices: batch.indices,
|
|
77
|
+
...(batch.fields === undefined ? {} : { fields: batch.fields }),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
67
81
|
export function toServerResponse(response) {
|
|
68
82
|
return HttpServerResponse.jsonUnsafe(errorBodyForWire(response.body), {
|
|
69
83
|
status: response.status,
|
|
@@ -323,6 +337,7 @@ export function expectedFailure(error, requestId) {
|
|
|
323
337
|
detail: message,
|
|
324
338
|
requestId,
|
|
325
339
|
issues: [{ path: [], message }],
|
|
340
|
+
...batchDetails(error),
|
|
326
341
|
},
|
|
327
342
|
};
|
|
328
343
|
}
|
|
@@ -421,6 +436,7 @@ export function expectedFailure(error, requestId) {
|
|
|
421
436
|
requestId,
|
|
422
437
|
...(typeof entity === "string" ? { entity } : {}),
|
|
423
438
|
...(typeof id === "string" ? { id } : {}),
|
|
439
|
+
...batchDetails(error),
|
|
424
440
|
},
|
|
425
441
|
};
|
|
426
442
|
}
|
|
@@ -434,6 +450,7 @@ export function expectedFailure(error, requestId) {
|
|
|
434
450
|
requestId,
|
|
435
451
|
...(typeof entity === "string" ? { entity } : {}),
|
|
436
452
|
...(typeof property === "string" ? { property } : {}),
|
|
453
|
+
...batchDetails(error),
|
|
437
454
|
},
|
|
438
455
|
};
|
|
439
456
|
}
|
package/dist/service/schemas.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BlobValue } from "../blob.js";
|
|
2
2
|
import { Schema, SchemaTransformation } from "effect";
|
|
3
3
|
import { wireStruct } from "../wire.js";
|
|
4
|
+
import { BatchErrorDetails } from "../errors.js";
|
|
4
5
|
import { AGGREGATE_FILTER_OPERATORS, ENTITY_AGGREGATE_FILTER_OPERATORS, filterValuesIssue, MAXIMUM_FILTER_VALUES, } from "../aggregate-filters.js";
|
|
5
6
|
import { isValueMatched, OntologyManifestWire } from "../manifest.js";
|
|
6
7
|
import { entityQuerySchemas, queryLinks } from "../query.js";
|
|
@@ -510,14 +511,17 @@ export const ErrorSchemas = {
|
|
|
510
511
|
notFound: errorBody("NotFound", 404, ["not_found"], {
|
|
511
512
|
entity: Schema.optionalKey(Schema.String),
|
|
512
513
|
id: Schema.optionalKey(Schema.String),
|
|
514
|
+
batch: Schema.optionalKey(BatchErrorDetails),
|
|
513
515
|
}),
|
|
514
516
|
conflict: errorBody("Conflict", 409, ["conflict"], {
|
|
515
517
|
entity: Schema.optionalKey(Schema.String),
|
|
516
518
|
property: Schema.optionalKey(Schema.String),
|
|
519
|
+
batch: Schema.optionalKey(BatchErrorDetails),
|
|
517
520
|
}),
|
|
518
521
|
unsupportedMediaType: errorBody("UnsupportedMediaType", 415, ["unsupported_media_type"]),
|
|
519
522
|
unprocessable: errorBody("Unprocessable", 422, ["invalid_input", "action_rejected"], {
|
|
520
523
|
issues: Schema.Array(ValidationIssue),
|
|
524
|
+
batch: Schema.optionalKey(BatchErrorDetails),
|
|
521
525
|
}),
|
|
522
526
|
rateLimited: errorBody("RateLimited", 429, ["rate_limited"]),
|
|
523
527
|
internal: errorBody("InternalError", 500, ["internal_error"]),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segmentstream-cloud/ontology",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Define a SegmentStream workspace app's ontology: entities, relationships, roles and actions, written with Effect.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SegmentStream",
|
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
"lint": "eslint src tests --config ../../eslint.config.mjs",
|
|
46
46
|
"test": "vitest run",
|
|
47
47
|
"test:postgres": "vitest run --config vitest.postgres.config.ts",
|
|
48
|
-
"prepack": "npm run build"
|
|
48
|
+
"prepack": "npm run build",
|
|
49
|
+
"benchmark:batch": "tsx tests/batch/benchmark.ts",
|
|
50
|
+
"benchmark:batch:profile": "tsx tests/batch/profile.ts"
|
|
49
51
|
},
|
|
50
52
|
"dependencies": {
|
|
51
53
|
"@aws-sdk/client-s3": "^3.1148.0",
|