marcidb-client 0.14.1 → 0.15.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.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "marcidb-client",
3
3
  "type": "module",
4
- "version": "0.14.1",
4
+ "version": "0.15.0",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "den59k"
@@ -207,6 +207,8 @@ type MarciOp = {
207
207
  query?: any;
208
208
  data?: any;
209
209
  id?: any;
210
+ key?: any;
211
+ onInsert?: any;
210
212
  journal?: JournalArgs;
211
213
  };
212
214
  /** The arguments of a journal action (`$journalOpen` / `$journalRead` / `$journalDrop`). `wait` is in seconds. */
@@ -275,9 +277,11 @@ interface Query<
275
277
  readonly [__sel]: Effective<T, Sel>;
276
278
  /** Filter (marcidb `$where`). Repeated calls are ANDed. */
277
279
  where(where: T["where"]): Query<T, Sel>;
278
- /** Sort by one field: `order("age", "desc")` or `order({ age: "desc" })`. */
280
+ /** Sort: `order("age", "desc")` or `order({ age: "desc" })`. A second `order()` adds the next key — ties on
281
+ * the first are broken by it (`order("createdAt", "desc").order("id")`); the keys may also come as an array
282
+ * in one call. Several keys sort in memory. */
279
283
  order(field: keyof T["order"] & string, direction?: "asc" | "desc"): Query<T, Sel>;
280
- order(order: T["order"]): Query<T, Sel>;
284
+ order(order: T["order"] | T["order"][]): Query<T, Sel>;
281
285
  limit(n: number): Query<T, Sel>;
282
286
  skip(n: number): Query<T, Sel>;
283
287
  /** Keyset cursor: rows strictly after this id in the current order (`$cursor`). */
@@ -302,7 +306,26 @@ interface Query<
302
306
  }>;
303
307
  aggregate<A extends T["aggregate"]>(query: A): Op<AggregateResult<T["model"], A>> & Sub<T["name"], NoInfer<A>>;
304
308
  insert(data: T["insert"]): Op<T["id"]>;
309
+ /** The new row by `select`'s shape — its defaults, dates and files as stored: one call, nothing to read back. */
310
+ insert<S extends T["query"]>(data: T["insert"], options: {
311
+ select: S;
312
+ }): Promise<Rows<T, S>>;
305
313
  update(id: IdArg<T["id"]>, data: T["update"]): Op<void>;
314
+ /** Writes the row `key` names — the id, or the fields of ONE unique index (a `@unique` field, all the fields of
315
+ * a `@unique(<name>)` group): an update of `data` when it exists, an insert of key + data when not. `data` is
316
+ * in the insert form (plain values, `{ id }` for a relation; no `$` operators). `onInsert` adds fields used
317
+ * only when the row is created. Resolves to the id, like `insert`. */
318
+ upsert<K extends Partial<T["insert"]> | T["id"]>(key: K, data: Omit<T["insert"], keyof K>, options?: {
319
+ onInsert?: Partial<T["insert"]>;
320
+ }): Op<T["id"]>;
321
+ /** The row written, by `select`'s shape — as stored, whichever path the upsert took. */
322
+ upsert<
323
+ K extends Partial<T["insert"]> | T["id"],
324
+ S extends T["query"]
325
+ >(key: K, data: Omit<T["insert"], keyof K>, options: {
326
+ onInsert?: Partial<T["insert"]>;
327
+ select: S;
328
+ }): Promise<Rows<T, S>>;
306
329
  /** Applies `data` to every row the chain's `where` matches (all rows without one); resolves to the number of rows. */
307
330
  updateMany(data: T["update"]): Op<number>;
308
331
  /** @deprecated pass the filter through the chain: `db.user.where(w).updateMany(data)` (removed in the next minor). */
@@ -376,6 +399,9 @@ type QueryLayerOptions = {
376
399
  run(op: MarciOp): Promise<any>;
377
400
  /** Hook for `insert` payloads (the lecodes SDK generates uuid ids here). */
378
401
  prepareInsert?(model: string, data: any): any;
402
+ /** Hook for the `onInsert` part of an `upsert` — the fields used only when the row is created (the lecodes SDK
403
+ * puts a uuid id here). Receives what the caller gave (or undefined), returns what is sent. */
404
+ prepareUpsert?(model: string, onInsert: any): any;
379
405
  };
380
406
  /**
381
407
  * The query builder over a set of models and a `run` function. Returns `op(descriptor)` — a lazy operation
@@ -386,6 +412,17 @@ declare function createQueryLayer(options: QueryLayerOptions): {
386
412
  op: (descriptor: MarciOp) => any;
387
413
  collection: (model: string) => any;
388
414
  };
415
+ /** The class of a refused request — the server's `kind`: a constraint (`unique`, `foreign_key`, `restrict`,
416
+ * `duplicate_id`), `not_found`, `bad_request`, `unauthorized`, or `internal` for a fault of the server. */
417
+ type MarciErrorKind = "unique" | "foreign_key" | "restrict" | "duplicate_id" | "not_found" | "bad_request" | "unauthorized" | "internal";
418
+ /** A failed request: `kind` is what to switch on, `field` the `Model.field` a constraint is about, the message is for a person. */
419
+ declare class MarciError extends Error {
420
+ name: "MarciError";
421
+ readonly status: number;
422
+ readonly kind: MarciErrorKind;
423
+ readonly field?: string;
424
+ constructor(status: number, message: string, kind: MarciErrorKind, field?: string);
425
+ }
389
426
  declare function request(method: string, url: string, body?: any): Promise<any>;
390
427
  /**
391
428
  * A binary-eligible read over HTTP. Advertises `Accept: <binary>, json` plus the client's schema
@@ -408,4 +445,4 @@ declare function createDecoderRegistry(MODELS: Record<string, FieldDesc[]>): {
408
445
  decodeBuffer(decoder: (reader: any) => any, bytes: Uint8Array, many: boolean): any;
409
446
  };
410
447
  declare function encodeId(id: any): string;
411
- export { AggregateKeys, AggregateResult, BareId, Collection, CompareNumValue, CompareRefListValue, CompareRefValue, CompareStrValue, CompareValue, CustomSearch, CustomSearchValue, Effective, FieldDesc, FieldOps, FullTextSearch, GetResult, IdArg, Journal, JournalArgs, JournalEntry, JournalOp, JournalOptions, JsonCondition, JsonPathWhere, JsonType, JsonValue, MarciOp, MarciTransport, ModelTypes, ModelsMeta, Never, NumOps, Only, Op, PrimitiveListUpdate, Query, QueryLayerOptions, RefListUpdate, RefListUpdateOrdered, RefListUpdateStruct, RefUpdate, RefUpdateStruct, Resolve, Rows, ServiceKeys, StrOps, Sub, UpdateNumValue, ValueOps, VectorSearch, WhereValue, createDecoderRegistry, createQueryLayer, encodeId, request, requestBinary };
448
+ export { AggregateKeys, AggregateResult, BareId, Collection, CompareNumValue, CompareRefListValue, CompareRefValue, CompareStrValue, CompareValue, CustomSearch, CustomSearchValue, Effective, FieldDesc, FieldOps, FullTextSearch, GetResult, IdArg, Journal, JournalArgs, JournalEntry, JournalOp, JournalOptions, JsonCondition, JsonPathWhere, JsonType, JsonValue, MarciError, MarciErrorKind, MarciOp, MarciTransport, ModelTypes, ModelsMeta, Never, NumOps, Only, Op, PrimitiveListUpdate, Query, QueryLayerOptions, RefListUpdate, RefListUpdateOrdered, RefListUpdateStruct, RefUpdate, RefUpdateStruct, Resolve, Rows, ServiceKeys, StrOps, Sub, UpdateNumValue, ValueOps, VectorSearch, WhereValue, createDecoderRegistry, createQueryLayer, encodeId, request, requestBinary };
package/runtime/index.js CHANGED
@@ -101,6 +101,11 @@ function createQueryLayer(options) {
101
101
  return w ? { $where: w } : {};
102
102
  };
103
103
  const findMany = () => ({ model, action: "findMany", query: build() });
104
+ const readBack = (written, select) => written.then((id) => run({ model, action: "findFirst", query: resolveShape(model, { ...select, $where: id }) })).then((row) => {
105
+ if (row === null)
106
+ throw new Error(`marcidb: ${model}: the row just written is gone`);
107
+ return row;
108
+ });
104
109
  return {
105
110
  get __op() {
106
111
  return findMany();
@@ -112,7 +117,12 @@ function createQueryLayer(options) {
112
117
  catch: (onRejected) => run(findMany()).catch(onRejected),
113
118
  finally: (onFinally) => run(findMany()).finally(onFinally),
114
119
  where: (where) => make({ ...st, where: and(st.where, where) }),
115
- order: (field, direction) => make({ ...st, order: typeof field === "string" ? { [field]: direction ?? "asc" } : field }),
120
+ order: (field, direction) => {
121
+ const given = typeof field === "string" ? [{ [field]: direction ?? "asc" }] : Array.isArray(field) ? field : Object.entries(field).map(([k, v]) => ({ [k]: v }));
122
+ const prev = st.order === undefined ? [] : Array.isArray(st.order) ? st.order : [st.order];
123
+ const keys = [...prev, ...given];
124
+ return make({ ...st, order: keys.length === 1 ? keys[0] : keys });
125
+ },
116
126
  limit: (n) => make({ ...st, limit: n }),
117
127
  skip: (n) => make({ ...st, skip: n }),
118
128
  after: (id) => make({ ...st, cursor: idObject(model, id) }),
@@ -128,8 +138,16 @@ function createQueryLayer(options) {
128
138
  delete q.$where;
129
139
  return Object.assign(op({ model, action: "aggregate", query: q }), { __select: q });
130
140
  },
131
- insert: (data) => op({ model, action: "insert", data: options.prepareInsert ? options.prepareInsert(model, data) : data }),
141
+ insert: (data, extra) => {
142
+ const inserted = op({ model, action: "insert", data: options.prepareInsert ? options.prepareInsert(model, data) : data });
143
+ return extra?.select ? readBack(inserted, extra.select) : inserted;
144
+ },
132
145
  update: (id, data) => op({ model, action: "update", id: idObject(model, id), data }),
146
+ upsert: (key, data, o) => {
147
+ const onInsert = options.prepareUpsert ? options.prepareUpsert(model, o?.onInsert) : o?.onInsert;
148
+ const written = op({ model, action: "upsert", key, data, ...onInsert !== undefined ? { onInsert } : {} });
149
+ return o?.select ? readBack(written, o.select) : written;
150
+ },
133
151
  updateMany: (a, b) => b === undefined ? op({ model, action: "updateMany", query: whereOnly(), data: a }) : op({ model, action: "updateMany", query: whereOnly(a), data: b }),
134
152
  delete: (id) => op({ model, action: "delete", id: idObject(model, id) }),
135
153
  deleteMany: () => {
@@ -149,16 +167,36 @@ function createQueryLayer(options) {
149
167
  }
150
168
 
151
169
  // src/index.ts
170
+ class MarciError extends Error {
171
+ name = "MarciError";
172
+ status;
173
+ kind;
174
+ field;
175
+ constructor(status, message, kind, field) {
176
+ super(message);
177
+ this.status = status;
178
+ this.kind = kind;
179
+ this.field = field;
180
+ }
181
+ }
182
+ var kindOfStatus = (status) => status === 404 ? "not_found" : status === 401 ? "unauthorized" : status >= 500 ? "internal" : "bad_request";
183
+ async function failure(res) {
184
+ const text = await res.text();
185
+ try {
186
+ const body = JSON.parse(text);
187
+ if (body && typeof body.error === "string")
188
+ return new MarciError(res.status, body.error, body.kind ?? kindOfStatus(res.status), body.field);
189
+ } catch {}
190
+ return new MarciError(res.status, text || `HTTP ${res.status}`, kindOfStatus(res.status));
191
+ }
152
192
  async function request(method, url, body) {
153
193
  const res = await fetch(url, {
154
194
  method,
155
195
  headers: { "Content-Type": "application/json" },
156
196
  body: body !== undefined ? JSON.stringify(body) : undefined
157
197
  });
158
- if (!res.ok) {
159
- const text = await res.text();
160
- throw new Error(`MarciDB [${res.status}]: ${text}`);
161
- }
198
+ if (!res.ok)
199
+ throw await failure(res);
162
200
  return res.json();
163
201
  }
164
202
  var BINARY_MEDIA_TYPE = "application/x-marcidb-rows";
@@ -172,10 +210,8 @@ async function requestBinary(url, body, schemaHash) {
172
210
  },
173
211
  body: JSON.stringify(body ?? {})
174
212
  });
175
- if (!res.ok) {
176
- const text = await res.text();
177
- throw new Error(`MarciDB [${res.status}]: ${text}`);
178
- }
213
+ if (!res.ok)
214
+ throw await failure(res);
179
215
  const contentType = res.headers.get("content-type") ?? "";
180
216
  if (contentType.includes(BINARY_MEDIA_TYPE)) {
181
217
  return new Uint8Array(await res.arrayBuffer());
@@ -377,6 +413,7 @@ function encodeId(id) {
377
413
  return String(id);
378
414
  }
379
415
  export {
416
+ MarciError,
380
417
  createDecoderRegistry,
381
418
  createQueryLayer,
382
419
  encodeId,