@segmentstream-cloud/ontology 0.25.0 → 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/agent/attachments.d.ts +3 -2
- package/dist/service/agent/attachments.js +4 -2
- package/dist/service/agent/authentication.d.ts +3 -1
- package/dist/service/agent/authentication.js +10 -4
- package/dist/service/errors.js +17 -0
- package/dist/service/schemas.js +4 -0
- package/dist/storage/attachments.d.ts +8 -7
- package/dist/storage/attachments.js +83 -23
- package/dist/storage/tables.d.ts +2 -1
- package/dist/storage/tables.js +2 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -253,6 +253,84 @@ export default defineOntology({
|
|
|
253
253
|
`InvalidOntology` with every diagnostic when the definition has errors. Entities,
|
|
254
254
|
actions and select options need a description.
|
|
255
255
|
|
|
256
|
+
## Batch writes in actions
|
|
257
|
+
|
|
258
|
+
Stored entity handles expose `createMany(records, options?)`,
|
|
259
|
+
`updateMany([{ id, values }], options?)`, and `upsertMany({ on, records, returning? })`
|
|
260
|
+
inside authored action handlers. The action remains the permission and commit
|
|
261
|
+
boundary; a batch does not run other authored actions for its rows.
|
|
262
|
+
|
|
263
|
+
For example, an action can write sales plan lines by a declared compound unique
|
|
264
|
+
key:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
const PlanLine = entity("planLine", {
|
|
268
|
+
id: "ent_plan_line",
|
|
269
|
+
label: "Plan line",
|
|
270
|
+
plural: "Plan lines",
|
|
271
|
+
title: "planCode",
|
|
272
|
+
description: "A sales plan decision for a period.",
|
|
273
|
+
properties: {
|
|
274
|
+
planCode: p.text({ description: "The plan code." }),
|
|
275
|
+
period: p.date({ description: "The period start." }),
|
|
276
|
+
target: p.number({ description: "The agreed target." }),
|
|
277
|
+
},
|
|
278
|
+
indexes: [{ properties: ["planCode", "period"], unique: true }],
|
|
279
|
+
}).actions((self) => ({
|
|
280
|
+
setTargets: action({
|
|
281
|
+
description: "Set targets for several plan lines together.",
|
|
282
|
+
input: Schema.Struct({
|
|
283
|
+
lines: Schema.Array(Schema.Struct(self.properties)),
|
|
284
|
+
}),
|
|
285
|
+
handler: ({ input }) =>
|
|
286
|
+
self.upsertMany({
|
|
287
|
+
on: ["planCode", "period"],
|
|
288
|
+
records: input.lines,
|
|
289
|
+
returning: "summary",
|
|
290
|
+
}),
|
|
291
|
+
}),
|
|
292
|
+
}));
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Both result modes return `{ created, updated, unchanged }`, whose counts sum to
|
|
296
|
+
the input length. The default, `returning: "records"`, also returns `items` in
|
|
297
|
+
input order: `{ index, status, record }`, with `status` equal to `"created"`,
|
|
298
|
+
`"updated"`, or `"unchanged"`. `returning: "summary"` omits `items`; the enclosing
|
|
299
|
+
action still returns its normal `edits`. These types are exported as
|
|
300
|
+
`BatchRecordResult<Record>`, `BatchSummaryResult`, and `BatchResult<Record>`.
|
|
301
|
+
|
|
302
|
+
Creates retain normal defaults and validation. Updates patch distinct IDs:
|
|
303
|
+
omitted fields remain unchanged, and explicit `null` follows the property's
|
|
304
|
+
nullability. An unchanged patch preserves timestamps and attribution and adds no
|
|
305
|
+
history or replication work. Upsert keys must be a nonempty list of ordinary
|
|
306
|
+
properties matching a declared unique property or compound unique index. Each
|
|
307
|
+
input explicitly supplies non-null key values and enough values to create a
|
|
308
|
+
record; existing records receive only supplied non-key fields. Duplicate IDs or
|
|
309
|
+
upsert keys fail the batch.
|
|
310
|
+
|
|
311
|
+
Batch values support ordinary properties and stored `rel.one` assignments. Blob
|
|
312
|
+
and `rel.many` assignments are rejected; use the existing single-record methods
|
|
313
|
+
for those fields. Warehouse-backed and platform-owned built-in entities are not
|
|
314
|
+
batch write targets. `OntologyWrites` exposes the same operations with an
|
|
315
|
+
explicit `entity` and snake_case field names; entity handles convert authored
|
|
316
|
+
camelCase names and select values.
|
|
317
|
+
|
|
318
|
+
Each call accepts at most 200 records and 2 MiB of UTF-8 JSON input, including
|
|
319
|
+
record values, update IDs, and upsert key fields. One action may attempt at most
|
|
320
|
+
500 rows across nonempty batch calls, including failed calls its handler catches.
|
|
321
|
+
Empty input returns zero counts and, in record mode, empty `items`, without SQL.
|
|
322
|
+
Every nonempty batch uses one savepoint inside the action transaction. A failure
|
|
323
|
+
rolls back all of that batch's records, history, replication references, and
|
|
324
|
+
edits; a handler may catch it and continue. A later uncaught action failure also
|
|
325
|
+
rolls back earlier successful batches.
|
|
326
|
+
|
|
327
|
+
Existing `ActionRejected`, `Conflict`, and `NotFound` errors may include
|
|
328
|
+
`batch: { indices, fields? }`: zero-based input positions and safe wire field
|
|
329
|
+
names. Batch calls are never automatically retried. Checkpoint a loader only
|
|
330
|
+
after confirmed action success; replay after a lost response is safe only when
|
|
331
|
+
the complete action is explicitly idempotent, including its other writes and
|
|
332
|
+
side effects. Generic upsert alone does not establish that guarantee.
|
|
333
|
+
|
|
256
334
|
## Permissions
|
|
257
335
|
|
|
258
336
|
A permission is `<entity>:<verb>`. The verbs are `read`, the generated `create`, `update`,
|
package/dist/entity.d.ts
CHANGED
|
@@ -3,10 +3,10 @@ import { Effect, type Schema } from "effect";
|
|
|
3
3
|
import type { Measures } from "./measure.js";
|
|
4
4
|
import type { WarehouseSource } from "./source.js";
|
|
5
5
|
import type { Actions } from "./action.js";
|
|
6
|
-
import type { Conflict, NotFound } from "./errors.js";
|
|
6
|
+
import type { ActionRejected, Conflict, NotFound } from "./errors.js";
|
|
7
7
|
import { type AnyProperty } from "./property.js";
|
|
8
8
|
import type { AnyRelationship, ManyRelationship, OneRelationship } from "./relationship.js";
|
|
9
|
-
import { OntologyReads, OntologyWrites, type FindOptions, type GetOptions, type UpsertResult } from "./runtime.js";
|
|
9
|
+
import { OntologyReads, OntologyWrites, type BatchOptions, type BatchRecordResult, type BatchResult, type BatchSummaryResult, type FindOptions, type GetOptions, type UpsertResult } from "./runtime.js";
|
|
10
10
|
export declare const EntityTypeId: unique symbol;
|
|
11
11
|
export type Properties = {
|
|
12
12
|
readonly [name: string]: AnyProperty;
|
|
@@ -98,6 +98,33 @@ export type UpdateValues<P, R> = Simplify<{
|
|
|
98
98
|
} & {
|
|
99
99
|
readonly [K in ManyKeys<R>]?: ReadonlyArray<string>;
|
|
100
100
|
}>;
|
|
101
|
+
type BlobKeys<P> = {
|
|
102
|
+
[K in keyof P]-?: NonNullable<ValueOf<P[K]>> extends BlobValue ? K : never;
|
|
103
|
+
}[keyof P] & string;
|
|
104
|
+
type UnsupportedBatchKeys<P, R> = BlobKeys<P> | ManyKeys<R>;
|
|
105
|
+
/** Create values for a batch: ordinary properties and stored `rel.one` assignments only. */
|
|
106
|
+
export type BatchCreateValues<P, R> = Simplify<Omit<CreateValues<P, R>, UnsupportedBatchKeys<P, R>> & {
|
|
107
|
+
readonly [K in UnsupportedBatchKeys<P, R>]?: never;
|
|
108
|
+
}>;
|
|
109
|
+
/** Patch values for a batch. Blob and collection assignments use single-record methods. */
|
|
110
|
+
export type BatchUpdateValues<P, R> = Simplify<Omit<UpdateValues<P, R>, UnsupportedBatchKeys<P, R>> & {
|
|
111
|
+
readonly [K in UnsupportedBatchKeys<P, R>]?: never;
|
|
112
|
+
}>;
|
|
113
|
+
/** Ordinary declared properties accepted as upsert keys; unique declarations are checked at runtime. */
|
|
114
|
+
export type BatchKey<P> = Exclude<keyof P & string, BlobKeys<P>>;
|
|
115
|
+
/** Upsert inputs must explicitly supply non-null values for every selected conflict key. */
|
|
116
|
+
export type BatchUpsertValues<P, R, K extends BatchKey<P>> = Simplify<Omit<BatchCreateValues<P, R>, K> & {
|
|
117
|
+
readonly [Key in K]: NonNullable<WriteValueOf<P[Key]>>;
|
|
118
|
+
}>;
|
|
119
|
+
export interface BatchUpdateRecord<P, R> {
|
|
120
|
+
readonly id: string;
|
|
121
|
+
readonly values: BatchUpdateValues<P, R>;
|
|
122
|
+
}
|
|
123
|
+
interface EntityUpsertManyInput<P, R, K extends BatchKey<P>> extends BatchOptions {
|
|
124
|
+
readonly on: readonly [K, ...K[]];
|
|
125
|
+
readonly records: ReadonlyArray<BatchUpsertValues<P, R, NoInfer<K>>>;
|
|
126
|
+
}
|
|
127
|
+
type BatchWriteError = ActionRejected | Conflict | NotFound;
|
|
101
128
|
/**
|
|
102
129
|
* Fields an index may cover: declared properties, system properties and `rel.one` relationships
|
|
103
130
|
* between stored entities.
|
|
@@ -172,6 +199,30 @@ export interface Entity<Name extends string = string, P extends Properties = Pro
|
|
|
172
199
|
create(values: CreateValues<P, R>): Effect.Effect<EntityRecord<P, R>, Conflict | NotFound, OntologyWrites>;
|
|
173
200
|
update(record: RecordRef, values: UpdateValues<P, R>): Effect.Effect<EntityRecord<P, R>, Conflict | NotFound, OntologyWrites>;
|
|
174
201
|
upsertBy(property: UniqueKeys<P>, values: CreateValues<P, R>): Effect.Effect<UpsertResult<EntityRecord<P, R>>, Conflict | NotFound, OntologyWrites>;
|
|
202
|
+
/** Atomically create records. Defaults to returning one record result per input, in order. */
|
|
203
|
+
createMany(records: ReadonlyArray<BatchCreateValues<P, R>>, options: {
|
|
204
|
+
readonly returning: "summary";
|
|
205
|
+
}): Effect.Effect<BatchSummaryResult, BatchWriteError, OntologyWrites>;
|
|
206
|
+
createMany(records: ReadonlyArray<BatchCreateValues<P, R>>, options?: {
|
|
207
|
+
readonly returning?: "records";
|
|
208
|
+
}): Effect.Effect<BatchRecordResult<EntityRecord<P, R>>, BatchWriteError, OntologyWrites>;
|
|
209
|
+
createMany(records: ReadonlyArray<BatchCreateValues<P, R>>, options?: BatchOptions): Effect.Effect<BatchResult<EntityRecord<P, R>>, BatchWriteError, OntologyWrites>;
|
|
210
|
+
/** Atomically patch distinct record IDs; values equal to the locked record remain unchanged. */
|
|
211
|
+
updateMany(records: ReadonlyArray<BatchUpdateRecord<P, R>>, options: {
|
|
212
|
+
readonly returning: "summary";
|
|
213
|
+
}): Effect.Effect<BatchSummaryResult, BatchWriteError, OntologyWrites>;
|
|
214
|
+
updateMany(records: ReadonlyArray<BatchUpdateRecord<P, R>>, options?: {
|
|
215
|
+
readonly returning?: "records";
|
|
216
|
+
}): Effect.Effect<BatchRecordResult<EntityRecord<P, R>>, BatchWriteError, OntologyWrites>;
|
|
217
|
+
updateMany(records: ReadonlyArray<BatchUpdateRecord<P, R>>, options?: BatchOptions): Effect.Effect<BatchResult<EntityRecord<P, R>>, BatchWriteError, OntologyWrites>;
|
|
218
|
+
/** Atomically insert or patch by a declared unique property or compound unique index. */
|
|
219
|
+
upsertMany<const K extends BatchKey<P>>(input: EntityUpsertManyInput<P, R, K> & {
|
|
220
|
+
readonly returning: "summary";
|
|
221
|
+
}): Effect.Effect<BatchSummaryResult, BatchWriteError, OntologyWrites>;
|
|
222
|
+
upsertMany<const K extends BatchKey<P>>(input: EntityUpsertManyInput<P, R, K> & {
|
|
223
|
+
readonly returning?: "records";
|
|
224
|
+
}): Effect.Effect<BatchRecordResult<EntityRecord<P, R>>, BatchWriteError, OntologyWrites>;
|
|
225
|
+
upsertMany<const K extends BatchKey<P>>(input: EntityUpsertManyInput<P, R, K>): Effect.Effect<BatchResult<EntityRecord<P, R>>, BatchWriteError, OntologyWrites>;
|
|
175
226
|
delete(record: RecordRef): Effect.Effect<void, Conflict | NotFound, OntologyWrites>;
|
|
176
227
|
link(relationship: ManyKeys<R>, record: RecordRef, target: RecordRef): Effect.Effect<void, NotFound, OntologyWrites>;
|
|
177
228
|
unlink(relationship: ManyKeys<R>, record: RecordRef, target: RecordRef): Effect.Effect<void, NotFound, OntologyWrites>;
|
package/dist/entity.js
CHANGED
|
@@ -19,6 +19,34 @@ export function entity(name, definition) {
|
|
|
19
19
|
measures: definition.measures ?? {},
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
+
const batchFromWire = (result) => "items" in result
|
|
23
|
+
? {
|
|
24
|
+
...result,
|
|
25
|
+
items: result.items.map((item) => ({
|
|
26
|
+
...item,
|
|
27
|
+
record: recordFromWire(item.record, definition),
|
|
28
|
+
})),
|
|
29
|
+
}
|
|
30
|
+
: result;
|
|
31
|
+
const createMany = ((records, options) => withWrites((runtime) => runtime.createMany({
|
|
32
|
+
entity: toSnakeCase(name),
|
|
33
|
+
records: records.map((values) => valuesToWire(values, definition)),
|
|
34
|
+
...(options?.returning === undefined ? {} : { returning: options.returning }),
|
|
35
|
+
})).pipe(Effect.map(batchFromWire)));
|
|
36
|
+
const updateMany = ((records, options) => withWrites((runtime) => runtime.updateMany({
|
|
37
|
+
entity: toSnakeCase(name),
|
|
38
|
+
records: records.map(({ id, values }) => ({
|
|
39
|
+
id,
|
|
40
|
+
values: valuesToWire(values, definition),
|
|
41
|
+
})),
|
|
42
|
+
...(options?.returning === undefined ? {} : { returning: options.returning }),
|
|
43
|
+
})).pipe(Effect.map(batchFromWire)));
|
|
44
|
+
const upsertMany = ((input) => withWrites((runtime) => runtime.upsertMany({
|
|
45
|
+
entity: toSnakeCase(name),
|
|
46
|
+
on: input.on.map(toSnakeCase),
|
|
47
|
+
records: input.records.map((values) => valuesToWire(values, definition)),
|
|
48
|
+
...(input.returning === undefined ? {} : { returning: input.returning }),
|
|
49
|
+
})).pipe(Effect.map(batchFromWire)));
|
|
22
50
|
const handle = {
|
|
23
51
|
[EntityTypeId]: EntityTypeId,
|
|
24
52
|
name,
|
|
@@ -26,6 +54,9 @@ export function entity(name, definition) {
|
|
|
26
54
|
properties: definition.properties,
|
|
27
55
|
relationships: definition.relationships ?? {},
|
|
28
56
|
ref: p.ref(name),
|
|
57
|
+
createMany,
|
|
58
|
+
updateMany,
|
|
59
|
+
upsertMany,
|
|
29
60
|
get: (record, options) => withReads((runtime) => runtime.get({
|
|
30
61
|
entity: toSnakeCase(name),
|
|
31
62
|
id: idOf(record),
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
+
/** Safe batch failure locations: zero-based input indices and wire field names. */
|
|
3
|
+
export declare const BatchErrorDetails: Schema.Struct<{
|
|
4
|
+
readonly indices: Schema.$Array<Schema.Int>;
|
|
5
|
+
readonly fields: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
6
|
+
}>;
|
|
7
|
+
export type BatchErrorDetails = typeof BatchErrorDetails.Type;
|
|
2
8
|
declare const ActionRejected_base: Schema.Class<ActionRejected, Schema.TaggedStruct<"ActionRejected", {
|
|
3
9
|
readonly message: Schema.String;
|
|
10
|
+
readonly batch: Schema.optionalKey<Schema.Struct<{
|
|
11
|
+
readonly indices: Schema.$Array<Schema.Int>;
|
|
12
|
+
readonly fields: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
13
|
+
}>>;
|
|
4
14
|
}>, import("effect/Cause").YieldableError>;
|
|
5
15
|
/**
|
|
6
16
|
* An action refused the request for a business reason. The message is written for the caller and is
|
|
@@ -23,6 +33,10 @@ declare const Conflict_base: Schema.Class<Conflict, Schema.TaggedStruct<"Conflic
|
|
|
23
33
|
readonly entity: Schema.String;
|
|
24
34
|
readonly message: Schema.String;
|
|
25
35
|
readonly property: Schema.optionalKey<Schema.String>;
|
|
36
|
+
readonly batch: Schema.optionalKey<Schema.Struct<{
|
|
37
|
+
readonly indices: Schema.$Array<Schema.Int>;
|
|
38
|
+
readonly fields: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
39
|
+
}>>;
|
|
26
40
|
}>, import("effect/Cause").YieldableError>;
|
|
27
41
|
/** The write conflicts with existing data, for example a duplicate value of a unique property. */
|
|
28
42
|
export declare class Conflict extends Conflict_base {
|
|
@@ -31,6 +45,10 @@ declare const NotFound_base: Schema.Class<NotFound, Schema.TaggedStruct<"NotFoun
|
|
|
31
45
|
readonly entity: Schema.String;
|
|
32
46
|
readonly id: Schema.String;
|
|
33
47
|
readonly message: Schema.String;
|
|
48
|
+
readonly batch: Schema.optionalKey<Schema.Struct<{
|
|
49
|
+
readonly indices: Schema.$Array<Schema.Int>;
|
|
50
|
+
readonly fields: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
51
|
+
}>>;
|
|
34
52
|
}>, import("effect/Cause").YieldableError>;
|
|
35
53
|
/** The record does not exist. */
|
|
36
54
|
export declare class NotFound extends NotFound_base {
|
package/dist/errors.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
+
/** Safe batch failure locations: zero-based input indices and wire field names. */
|
|
3
|
+
export const BatchErrorDetails = Schema.Struct({
|
|
4
|
+
indices: Schema.Array(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
5
|
+
fields: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
6
|
+
});
|
|
2
7
|
/**
|
|
3
8
|
* An action refused the request for a business reason. The message is written for the caller and is
|
|
4
9
|
* returned to them.
|
|
5
10
|
*/
|
|
6
11
|
export class ActionRejected extends Schema.TaggedError()("ActionRejected", {
|
|
7
12
|
message: Schema.String,
|
|
13
|
+
batch: Schema.optionalKey(BatchErrorDetails),
|
|
8
14
|
}) {
|
|
9
15
|
}
|
|
10
16
|
/**
|
|
@@ -22,6 +28,7 @@ export class Conflict extends Schema.TaggedError()("Conflict", {
|
|
|
22
28
|
entity: Schema.String,
|
|
23
29
|
message: Schema.String,
|
|
24
30
|
property: Schema.optionalKey(Schema.String),
|
|
31
|
+
batch: Schema.optionalKey(BatchErrorDetails),
|
|
25
32
|
}) {
|
|
26
33
|
}
|
|
27
34
|
/** The record does not exist. */
|
|
@@ -29,5 +36,6 @@ export class NotFound extends Schema.TaggedError()("NotFound", {
|
|
|
29
36
|
entity: Schema.String,
|
|
30
37
|
id: Schema.String,
|
|
31
38
|
message: Schema.String,
|
|
39
|
+
batch: Schema.optionalKey(BatchErrorDetails),
|
|
32
40
|
}) {
|
|
33
41
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,8 +13,8 @@ export type { EntityPermissions, JsonSchemaObject, MemberOptions, OntologyConfig
|
|
|
13
13
|
export { Diagnostic, DIAGNOSTIC_CODES, formatDiagnostics, InvalidOntology } from "./diagnostics.js";
|
|
14
14
|
export type { DiagnosticCode } from "./diagnostics.js";
|
|
15
15
|
export { entity, isEntity } from "./entity.js";
|
|
16
|
-
export type { ActionsBuilder, AnyEntity, AnyStoredEntity, CreateValues, DraftEntity, Entity, EntityDefinition, EntityRecord, IndexDefinition, IndexField, NoActions, Properties, RecordRef, Relationships, SystemFields, UpdateValues, } from "./entity.js";
|
|
17
|
-
export { ActionRejected, Conflict, NotFound, PermissionDenied } from "./errors.js";
|
|
16
|
+
export type { ActionsBuilder, AnyEntity, AnyStoredEntity, BatchCreateValues, BatchKey, BatchUpdateRecord, BatchUpdateValues, BatchUpsertValues, CreateValues, DraftEntity, Entity, EntityDefinition, EntityRecord, IndexDefinition, IndexField, NoActions, Properties, RecordRef, Relationships, SystemFields, UpdateValues, } from "./entity.js";
|
|
17
|
+
export { ActionRejected, BatchErrorDetails, Conflict, NotFound, PermissionDenied, } from "./errors.js";
|
|
18
18
|
export type { ActionError } from "./errors.js";
|
|
19
19
|
export { ActionManifest, actionConnectionKeys, canonicalJson, ConnectionManifest, EntityManifest, EntityPermissionsManifest, decodeManifestDocument, encodeManifest, formatManifest, IndexManifest, isValueMatched, MANIFEST_VERSION, manifestHash, ModelMeasureManifest, OntologyManifest, OntologyManifestWire, PropertyManifest, RelationshipManifest, RoleManifest, SelectOptionManifest, SignInManifest, signInPolicyOf, } from "./manifest.js";
|
|
20
20
|
export { SYSTEM_PROPERTIES } from "./names.js";
|
|
@@ -34,7 +34,7 @@ export type { Role, RoleOptions } from "./role.js";
|
|
|
34
34
|
export { DEFAULT_SIGN_IN_POLICY, SIGN_IN_METHODS, SIGN_IN_POLICIES } from "./sign-in.js";
|
|
35
35
|
export type { SignInCheck, SignInMethod, SignInOptions, SignInPolicy } from "./sign-in.js";
|
|
36
36
|
export { OntologyReads, OntologyRuntime, OntologyWrites } from "./runtime.js";
|
|
37
|
-
export type { FieldCondition, OntologyRuntimeService, FindOptions, GetOptions, Principal, RecordWhere, StoredRecord, StoredValues, UpsertResult, } from "./runtime.js";
|
|
37
|
+
export type { BatchOptions, BatchRecordResult, BatchResult, BatchReturning, BatchSummaryResult, CreateManyInput, FieldCondition, OntologyRuntimeService, FindOptions, GetOptions, Principal, RecordWhere, StoredRecord, StoredValues, UpsertResult, UpdateManyInput, UpsertManyInput, } from "./runtime.js";
|
|
38
38
|
export { User } from "./user.js";
|
|
39
39
|
export { AgentChannel, AgentChat } from "./agent-chat.js";
|
|
40
40
|
export type { AgentMessage } from "./agent-chat.js";
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export { action, isAction } from "./action.js";
|
|
|
6
6
|
export { CONNECTION_ERROR_REASONS, CONNECTION_KEY, CONNECTION_METHODS, connection, ConnectionError, Connections, ConnectionTransport, isConnection, MAXIMUM_CONNECTION_BODY_BYTES, } from "./connection.js";
|
|
7
7
|
export { Diagnostic, DIAGNOSTIC_CODES, formatDiagnostics, InvalidOntology } from "./diagnostics.js";
|
|
8
8
|
export { entity, isEntity } from "./entity.js";
|
|
9
|
-
export { ActionRejected, Conflict, NotFound, PermissionDenied } from "./errors.js";
|
|
9
|
+
export { ActionRejected, BatchErrorDetails, Conflict, NotFound, PermissionDenied, } from "./errors.js";
|
|
10
10
|
export { ActionManifest, actionConnectionKeys, canonicalJson, ConnectionManifest, EntityManifest, EntityPermissionsManifest, decodeManifestDocument, encodeManifest, formatManifest, IndexManifest, isValueMatched, MANIFEST_VERSION, manifestHash, ModelMeasureManifest, OntologyManifest, OntologyManifestWire, PropertyManifest, RelationshipManifest, RoleManifest, SelectOptionManifest, SignInManifest, signInPolicyOf, } from "./manifest.js";
|
|
11
11
|
export { SYSTEM_PROPERTIES } from "./names.js";
|
|
12
12
|
export { check, defineOntology } from "./ontology.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { type SqlError } from "effect/unstable/sql/SqlError";
|
|
3
|
+
import type { EntityModel } from "./model.js";
|
|
4
|
+
interface CauseDiagnostic {
|
|
5
|
+
readonly tag: string;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly messageRedacted: boolean;
|
|
8
|
+
readonly stack?: string;
|
|
9
|
+
}
|
|
10
|
+
/** A safe projection of every wrapper; the original driver cause stays on the Conflict itself. */
|
|
11
|
+
export declare function batchConflictDiagnostic(entity: EntityModel, error: SqlError): {
|
|
12
|
+
causeChain: CauseDiagnostic[];
|
|
13
|
+
constraint?: string | undefined;
|
|
14
|
+
sqlState: string;
|
|
15
|
+
};
|
|
16
|
+
/** Emit before mapping the error, so even a handler-caught conflict has a correlated diagnostic. */
|
|
17
|
+
export declare function logBatchConflict(entity: EntityModel, error: SqlError, scope: {
|
|
18
|
+
readonly operation: string;
|
|
19
|
+
readonly action: string;
|
|
20
|
+
readonly actionId: string;
|
|
21
|
+
}): Effect.Effect<void>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { isSqlError, isSqlErrorReason } from "effect/unstable/sql/SqlError";
|
|
3
|
+
import { fieldColumnName, indexName, primaryKeyName, uniqueIndexName } from "../migrate/storage.js";
|
|
4
|
+
const DRIVER_MESSAGES = new Set([
|
|
5
|
+
"PgConnection: Query failed",
|
|
6
|
+
"PgConnection: The server reported an error",
|
|
7
|
+
]);
|
|
8
|
+
const FRAME_PREFIX = "^\\s+at (?:(?:async |new )?(?:[\\w.$<>]+ )?(?:\\[as [\\w$]+\\] )?\\()?";
|
|
9
|
+
const FRAME_LOCATION = "(?:(?:file:\\/\\/)?\\/[\\w./@%+~:-]+|node:[\\w./-]+)";
|
|
10
|
+
const SOURCE_FRAME = new RegExp(`${FRAME_PREFIX}${FRAME_LOCATION}:\\d+:\\d+\\)?$`);
|
|
11
|
+
function knownConstraint(entity, error) {
|
|
12
|
+
const cause = error.reason.cause;
|
|
13
|
+
const constraint = error.reason._tag === "UniqueViolation"
|
|
14
|
+
? error.reason.constraint
|
|
15
|
+
: typeof cause === "object" && cause !== null && "constraint" in cause
|
|
16
|
+
? cause.constraint
|
|
17
|
+
: undefined;
|
|
18
|
+
const names = [
|
|
19
|
+
primaryKeyName(entity.table),
|
|
20
|
+
primaryKeyName(entity.historyTable),
|
|
21
|
+
...entity.manifest.properties
|
|
22
|
+
.filter((field) => field.unique)
|
|
23
|
+
.map((field) => uniqueIndexName(entity.name, field.name)),
|
|
24
|
+
...entity.manifest.indexes
|
|
25
|
+
.filter((index) => index.unique)
|
|
26
|
+
.map((index) => indexName(entity.table, index.properties.map((name) => fieldColumnName(entity.manifest, name) ?? name))),
|
|
27
|
+
];
|
|
28
|
+
// PostgreSQL truncates these generated ASCII identifiers to its 63-byte identifier limit.
|
|
29
|
+
return typeof constraint === "string" && names.some((name) => name.slice(0, 63) === constraint)
|
|
30
|
+
? constraint
|
|
31
|
+
: undefined;
|
|
32
|
+
}
|
|
33
|
+
function causeTag(cause) {
|
|
34
|
+
if (isSqlError(cause)) {
|
|
35
|
+
return "SqlError";
|
|
36
|
+
}
|
|
37
|
+
if (isSqlErrorReason(cause)) {
|
|
38
|
+
return cause._tag;
|
|
39
|
+
}
|
|
40
|
+
return cause instanceof Error ? "Error" : "UnknownCause";
|
|
41
|
+
}
|
|
42
|
+
function safeCause(cause, constraint) {
|
|
43
|
+
if (!(cause instanceof Error)) {
|
|
44
|
+
return { tag: causeTag(cause), message: "[redacted database cause]", messageRedacted: true };
|
|
45
|
+
}
|
|
46
|
+
const original = cause.message;
|
|
47
|
+
const knownUniqueMessage = constraint === undefined
|
|
48
|
+
? undefined
|
|
49
|
+
: `duplicate key value violates unique constraint "${constraint}"`;
|
|
50
|
+
const allowed = DRIVER_MESSAGES.has(original) || original === knownUniqueMessage;
|
|
51
|
+
const message = allowed ? original : "[redacted database message]";
|
|
52
|
+
const headline = `${cause.name}: ${original}`;
|
|
53
|
+
const stack = typeof cause.stack === "string" && cause.stack.startsWith(`${headline}\n`)
|
|
54
|
+
? cause.stack.slice(headline.length + 1)
|
|
55
|
+
: "";
|
|
56
|
+
// Remove the complete (possibly multiline) original headline before keeping JS source frames.
|
|
57
|
+
// SQL, hints, parameters and arbitrary database explanations never enter this projection.
|
|
58
|
+
const frames = stack.split("\n").filter((line) => SOURCE_FRAME.test(line));
|
|
59
|
+
return {
|
|
60
|
+
tag: causeTag(cause),
|
|
61
|
+
message,
|
|
62
|
+
messageRedacted: !allowed,
|
|
63
|
+
...(frames.length > 0 ? { stack: frames.join("\n") } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** A safe projection of every wrapper; the original driver cause stays on the Conflict itself. */
|
|
67
|
+
export function batchConflictDiagnostic(entity, error) {
|
|
68
|
+
const constraint = knownConstraint(entity, error);
|
|
69
|
+
const causeChain = [];
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
let current = error;
|
|
72
|
+
while (current !== undefined && !seen.has(current)) {
|
|
73
|
+
seen.add(current);
|
|
74
|
+
causeChain.push(safeCause(current, constraint));
|
|
75
|
+
current =
|
|
76
|
+
typeof current === "object" && current !== null && "cause" in current
|
|
77
|
+
? current.cause
|
|
78
|
+
: undefined;
|
|
79
|
+
}
|
|
80
|
+
return { sqlState: "23505", ...(constraint === undefined ? {} : { constraint }), causeChain };
|
|
81
|
+
}
|
|
82
|
+
/** Emit before mapping the error, so even a handler-caught conflict has a correlated diagnostic. */
|
|
83
|
+
export function logBatchConflict(entity, error, scope) {
|
|
84
|
+
return Effect.logWarning("ontology.action.batch_conflict").pipe(Effect.annotateLogs({
|
|
85
|
+
...scope,
|
|
86
|
+
entity: entity.name,
|
|
87
|
+
...batchConflictDiagnostic(entity, error),
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { SqlError } from "effect/unstable/sql/SqlError";
|
|
3
|
+
import { ActionRejected, Conflict, NotFound } from "../errors.js";
|
|
4
|
+
import type { BatchResult, BatchReturning, StoredValues } from "../runtime.js";
|
|
5
|
+
import { type EntityModel, type FieldModel } from "./model.js";
|
|
6
|
+
import { type PreparedWrite } from "./records.js";
|
|
7
|
+
import type { WriteContext } from "./writes.js";
|
|
8
|
+
export declare const BATCH_MAX_ROWS = 200;
|
|
9
|
+
export declare const BATCH_MAX_BYTES: number;
|
|
10
|
+
export declare const ACTION_MAX_BATCH_ROWS = 500;
|
|
11
|
+
/** The pinned PostgreSQL driver's signed Int16 count caps bindings at 32,767. */
|
|
12
|
+
export declare const BATCH_MAX_PARAMETERS = 30000;
|
|
13
|
+
/** Includes bound values and an allowance for their SQL placeholders. */
|
|
14
|
+
export declare const BATCH_MAX_STATEMENT_BYTES: number;
|
|
15
|
+
export type BatchOperation = "create" | "update" | "upsert";
|
|
16
|
+
export type BatchError = ActionRejected | Conflict | NotFound | SqlError;
|
|
17
|
+
export interface BatchInput {
|
|
18
|
+
readonly records: ReadonlyArray<StoredValues | {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly values: StoredValues;
|
|
21
|
+
}>;
|
|
22
|
+
readonly returning?: BatchReturning;
|
|
23
|
+
readonly on?: ReadonlyArray<string>;
|
|
24
|
+
}
|
|
25
|
+
interface PlannedRow {
|
|
26
|
+
readonly index: number;
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly create: PreparedWrite;
|
|
29
|
+
readonly patch: PreparedWrite;
|
|
30
|
+
}
|
|
31
|
+
export interface BatchPlan {
|
|
32
|
+
readonly operation: BatchOperation;
|
|
33
|
+
readonly returning: BatchReturning;
|
|
34
|
+
readonly rows: ReadonlyArray<PlannedRow>;
|
|
35
|
+
readonly keys: ReadonlyArray<FieldModel>;
|
|
36
|
+
readonly distinctReferences: number;
|
|
37
|
+
}
|
|
38
|
+
export interface BatchContext extends WriteContext {
|
|
39
|
+
/** Set only after a prior write's savepoint succeeded. */
|
|
40
|
+
readonly attributionInitialized: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** One UTF-8 JSON encoding defines the public byte bound, including IDs and conflict fields. */
|
|
43
|
+
export declare function batchInputBytes(input: BatchInput): number;
|
|
44
|
+
/** All authored-value validation finishes before a record mutation can execute. */
|
|
45
|
+
export declare function planBatch(entity: EntityModel, operation: BatchOperation, input: BatchInput): Effect.Effect<BatchPlan, ActionRejected | NotFound>;
|
|
46
|
+
/** Executes one set of mutations; the caller owns its single savepoint and edit publication. */
|
|
47
|
+
export declare function executeBatch(context: BatchContext, entity: EntityModel, plan: BatchPlan): Effect.Effect<BatchResult, BatchError>;
|
|
48
|
+
export {};
|