@atscript/moost-db 0.1.83 → 0.1.85

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/dist/index.cjs CHANGED
@@ -1510,7 +1510,8 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1510
1510
  fields,
1511
1511
  type: this.getSerializedType(),
1512
1512
  actions: this.buildActions(),
1513
- crud: this.buildCrud()
1513
+ crud: this.buildCrud(),
1514
+ versionColumn: this.readable.versionColumn
1514
1515
  };
1515
1516
  }
1516
1517
  buildCrud() {
@@ -1561,6 +1562,29 @@ registerAsDbReadableController(AsDbReadableController);
1561
1562
  //#endregion
1562
1563
  //#region src/as-db.controller.ts
1563
1564
  var _ref$2, _ref2$1;
1565
+ /**
1566
+ * Strips the version field from a write body and lifts it to `$cas`, in place.
1567
+ * Returns `true` iff a `$cas` predicate was actually attached — callers use
1568
+ * this to gate the 404/409 disambiguation `findOne`.
1569
+ *
1570
+ * Per §6.2 of VERSION_PROPOSAL.md, the moost-db controller treats `version` in
1571
+ * a write body as a `$cas` directive rather than a SET. No-op (returns `false`)
1572
+ * when:
1573
+ * - the payload isn't an object (rejected downstream by the SDK validator),
1574
+ * - the version field is absent (presence-based opt-out → last-write-wins),
1575
+ * - the value is not a finite number (the SDK will reject loudly via
1576
+ * `$cas` validation — we don't shadow that with a controller-local 400).
1577
+ */
1578
+ function liftVersionToCas(payload, versionColumn) {
1579
+ if (payload === null || typeof payload !== "object") return false;
1580
+ const obj = payload;
1581
+ if (!(versionColumn in obj)) return false;
1582
+ const versionValue = obj[versionColumn];
1583
+ if (typeof versionValue !== "number" || !Number.isFinite(versionValue)) return false;
1584
+ delete obj[versionColumn];
1585
+ obj.$cas = { [versionColumn]: versionValue };
1586
+ return true;
1587
+ }
1564
1588
  let AsDbController = class AsDbController extends AsDbReadableController {
1565
1589
  /** Reference to the underlying table (typed for write access). */
1566
1590
  get table() {
@@ -1607,29 +1631,66 @@ let AsDbController = class AsDbController extends AsDbReadableController {
1607
1631
  }
1608
1632
  /**
1609
1633
  * **PUT /** — fully replaces one or many records matched by primary key.
1634
+ *
1635
+ * When the table opts into OCC (`@db.column.version`), a top-level `version`
1636
+ * field in the body is auto-lifted to `$cas` (§6.2 of VERSION_PROPOSAL.md).
1637
+ * On `matchedCount === 0` for a CAS-protected write, this disambiguates
1638
+ * 404 (row gone) vs 409 (version mismatch) via a single `findOne`.
1610
1639
  */
1611
1640
  async replace(payload) {
1641
+ const versionColumn = this.table.versionColumn;
1612
1642
  if (Array.isArray(payload)) {
1613
1643
  const data = await this.onWrite("replaceMany", payload);
1614
1644
  if (data === void 0) return new _moostjs_event_http.HttpError(500, "Not saved");
1645
+ if (versionColumn !== void 0) for (const item of data) liftVersionToCas(item, versionColumn);
1615
1646
  return await this.table.bulkReplace(data);
1616
1647
  }
1617
1648
  const data = await this.onWrite("replace", payload);
1618
1649
  if (data === void 0) return new _moostjs_event_http.HttpError(500, "Not saved");
1619
- return await this.table.replaceOne(data);
1650
+ const hadCasLift = versionColumn !== void 0 && liftVersionToCas(data, versionColumn);
1651
+ const result = await this.table.replaceOne(data);
1652
+ if (hadCasLift && result.matchedCount === 0) return await this._disambiguateMismatch(data, versionColumn);
1653
+ return result;
1620
1654
  }
1621
1655
  /**
1622
1656
  * **PATCH /** — partially updates one or many records matched by primary key.
1657
+ *
1658
+ * Same OCC semantics as {@link replace} (§6.2 / §6.3).
1623
1659
  */
1624
1660
  async update(payload) {
1661
+ const versionColumn = this.table.versionColumn;
1625
1662
  if (Array.isArray(payload)) {
1626
1663
  const data = await this.onWrite("updateMany", payload);
1627
1664
  if (data === void 0) return new _moostjs_event_http.HttpError(500, "Not saved");
1665
+ if (versionColumn !== void 0) for (const item of data) liftVersionToCas(item, versionColumn);
1628
1666
  return await this.table.bulkUpdate(data);
1629
1667
  }
1630
1668
  const data = await this.onWrite("update", payload);
1631
1669
  if (data === void 0) return new _moostjs_event_http.HttpError(500, "Not saved");
1632
- return await this.table.updateOne(data);
1670
+ const hadCasLift = versionColumn !== void 0 && liftVersionToCas(data, versionColumn);
1671
+ const result = await this.table.updateOne(data);
1672
+ if (hadCasLift && result.matchedCount === 0) return await this._disambiguateMismatch(data, versionColumn);
1673
+ return result;
1674
+ }
1675
+ /**
1676
+ * Disambiguates a `matchedCount === 0` result on a CAS-protected write:
1677
+ * returns 404 when the row is genuinely missing, 409 with
1678
+ * `{ error: "version_mismatch", currentVersion: N }` when it's present
1679
+ * but the supplied version is stale (§6.3).
1680
+ */
1681
+ async _disambiguateMismatch(data, versionColumn) {
1682
+ const filter = this.table.resolveIdFilter(data);
1683
+ const row = filter ? await this.table.findOne({
1684
+ filter,
1685
+ controls: {}
1686
+ }) : null;
1687
+ if (row === null) return new _moostjs_event_http.HttpError(404);
1688
+ return new _moostjs_event_http.HttpError(409, {
1689
+ message: "version_mismatch",
1690
+ statusCode: 409,
1691
+ kind: "version_mismatch",
1692
+ currentVersion: row[versionColumn]
1693
+ });
1633
1694
  }
1634
1695
  /**
1635
1696
  * **DELETE /:id** — removes a single record by primary key.
package/dist/index.d.cts CHANGED
@@ -323,12 +323,26 @@ declare class AsDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotat
323
323
  insert(payload: unknown): Promise<unknown>;
324
324
  /**
325
325
  * **PUT /** — fully replaces one or many records matched by primary key.
326
+ *
327
+ * When the table opts into OCC (`@db.column.version`), a top-level `version`
328
+ * field in the body is auto-lifted to `$cas` (§6.2 of VERSION_PROPOSAL.md).
329
+ * On `matchedCount === 0` for a CAS-protected write, this disambiguates
330
+ * 404 (row gone) vs 409 (version mismatch) via a single `findOne`.
326
331
  */
327
332
  replace(payload: unknown): Promise<unknown>;
328
333
  /**
329
334
  * **PATCH /** — partially updates one or many records matched by primary key.
335
+ *
336
+ * Same OCC semantics as {@link replace} (§6.2 / §6.3).
330
337
  */
331
338
  update(payload: unknown): Promise<unknown>;
339
+ /**
340
+ * Disambiguates a `matchedCount === 0` result on a CAS-protected write:
341
+ * returns 404 when the row is genuinely missing, 409 with
342
+ * `{ error: "version_mismatch", currentVersion: N }` when it's present
343
+ * but the supplied version is stale (§6.3).
344
+ */
345
+ protected _disambiguateMismatch(data: unknown, versionColumn: string): Promise<HttpError>;
332
346
  /**
333
347
  * **DELETE /:id** — removes a single record by primary key.
334
348
  */
package/dist/index.d.mts CHANGED
@@ -323,12 +323,26 @@ declare class AsDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotat
323
323
  insert(payload: unknown): Promise<unknown>;
324
324
  /**
325
325
  * **PUT /** — fully replaces one or many records matched by primary key.
326
+ *
327
+ * When the table opts into OCC (`@db.column.version`), a top-level `version`
328
+ * field in the body is auto-lifted to `$cas` (§6.2 of VERSION_PROPOSAL.md).
329
+ * On `matchedCount === 0` for a CAS-protected write, this disambiguates
330
+ * 404 (row gone) vs 409 (version mismatch) via a single `findOne`.
326
331
  */
327
332
  replace(payload: unknown): Promise<unknown>;
328
333
  /**
329
334
  * **PATCH /** — partially updates one or many records matched by primary key.
335
+ *
336
+ * Same OCC semantics as {@link replace} (§6.2 / §6.3).
330
337
  */
331
338
  update(payload: unknown): Promise<unknown>;
339
+ /**
340
+ * Disambiguates a `matchedCount === 0` result on a CAS-protected write:
341
+ * returns 404 when the row is genuinely missing, 409 with
342
+ * `{ error: "version_mismatch", currentVersion: N }` when it's present
343
+ * but the supplied version is stale (§6.3).
344
+ */
345
+ protected _disambiguateMismatch(data: unknown, versionColumn: string): Promise<HttpError>;
332
346
  /**
333
347
  * **DELETE /:id** — removes a single record by primary key.
334
348
  */
package/dist/index.mjs CHANGED
@@ -1509,7 +1509,8 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1509
1509
  fields,
1510
1510
  type: this.getSerializedType(),
1511
1511
  actions: this.buildActions(),
1512
- crud: this.buildCrud()
1512
+ crud: this.buildCrud(),
1513
+ versionColumn: this.readable.versionColumn
1513
1514
  };
1514
1515
  }
1515
1516
  buildCrud() {
@@ -1560,6 +1561,29 @@ registerAsDbReadableController(AsDbReadableController);
1560
1561
  //#endregion
1561
1562
  //#region src/as-db.controller.ts
1562
1563
  var _ref$2, _ref2$1;
1564
+ /**
1565
+ * Strips the version field from a write body and lifts it to `$cas`, in place.
1566
+ * Returns `true` iff a `$cas` predicate was actually attached — callers use
1567
+ * this to gate the 404/409 disambiguation `findOne`.
1568
+ *
1569
+ * Per §6.2 of VERSION_PROPOSAL.md, the moost-db controller treats `version` in
1570
+ * a write body as a `$cas` directive rather than a SET. No-op (returns `false`)
1571
+ * when:
1572
+ * - the payload isn't an object (rejected downstream by the SDK validator),
1573
+ * - the version field is absent (presence-based opt-out → last-write-wins),
1574
+ * - the value is not a finite number (the SDK will reject loudly via
1575
+ * `$cas` validation — we don't shadow that with a controller-local 400).
1576
+ */
1577
+ function liftVersionToCas(payload, versionColumn) {
1578
+ if (payload === null || typeof payload !== "object") return false;
1579
+ const obj = payload;
1580
+ if (!(versionColumn in obj)) return false;
1581
+ const versionValue = obj[versionColumn];
1582
+ if (typeof versionValue !== "number" || !Number.isFinite(versionValue)) return false;
1583
+ delete obj[versionColumn];
1584
+ obj.$cas = { [versionColumn]: versionValue };
1585
+ return true;
1586
+ }
1563
1587
  let AsDbController = class AsDbController extends AsDbReadableController {
1564
1588
  /** Reference to the underlying table (typed for write access). */
1565
1589
  get table() {
@@ -1606,29 +1630,66 @@ let AsDbController = class AsDbController extends AsDbReadableController {
1606
1630
  }
1607
1631
  /**
1608
1632
  * **PUT /** — fully replaces one or many records matched by primary key.
1633
+ *
1634
+ * When the table opts into OCC (`@db.column.version`), a top-level `version`
1635
+ * field in the body is auto-lifted to `$cas` (§6.2 of VERSION_PROPOSAL.md).
1636
+ * On `matchedCount === 0` for a CAS-protected write, this disambiguates
1637
+ * 404 (row gone) vs 409 (version mismatch) via a single `findOne`.
1609
1638
  */
1610
1639
  async replace(payload) {
1640
+ const versionColumn = this.table.versionColumn;
1611
1641
  if (Array.isArray(payload)) {
1612
1642
  const data = await this.onWrite("replaceMany", payload);
1613
1643
  if (data === void 0) return new HttpError(500, "Not saved");
1644
+ if (versionColumn !== void 0) for (const item of data) liftVersionToCas(item, versionColumn);
1614
1645
  return await this.table.bulkReplace(data);
1615
1646
  }
1616
1647
  const data = await this.onWrite("replace", payload);
1617
1648
  if (data === void 0) return new HttpError(500, "Not saved");
1618
- return await this.table.replaceOne(data);
1649
+ const hadCasLift = versionColumn !== void 0 && liftVersionToCas(data, versionColumn);
1650
+ const result = await this.table.replaceOne(data);
1651
+ if (hadCasLift && result.matchedCount === 0) return await this._disambiguateMismatch(data, versionColumn);
1652
+ return result;
1619
1653
  }
1620
1654
  /**
1621
1655
  * **PATCH /** — partially updates one or many records matched by primary key.
1656
+ *
1657
+ * Same OCC semantics as {@link replace} (§6.2 / §6.3).
1622
1658
  */
1623
1659
  async update(payload) {
1660
+ const versionColumn = this.table.versionColumn;
1624
1661
  if (Array.isArray(payload)) {
1625
1662
  const data = await this.onWrite("updateMany", payload);
1626
1663
  if (data === void 0) return new HttpError(500, "Not saved");
1664
+ if (versionColumn !== void 0) for (const item of data) liftVersionToCas(item, versionColumn);
1627
1665
  return await this.table.bulkUpdate(data);
1628
1666
  }
1629
1667
  const data = await this.onWrite("update", payload);
1630
1668
  if (data === void 0) return new HttpError(500, "Not saved");
1631
- return await this.table.updateOne(data);
1669
+ const hadCasLift = versionColumn !== void 0 && liftVersionToCas(data, versionColumn);
1670
+ const result = await this.table.updateOne(data);
1671
+ if (hadCasLift && result.matchedCount === 0) return await this._disambiguateMismatch(data, versionColumn);
1672
+ return result;
1673
+ }
1674
+ /**
1675
+ * Disambiguates a `matchedCount === 0` result on a CAS-protected write:
1676
+ * returns 404 when the row is genuinely missing, 409 with
1677
+ * `{ error: "version_mismatch", currentVersion: N }` when it's present
1678
+ * but the supplied version is stale (§6.3).
1679
+ */
1680
+ async _disambiguateMismatch(data, versionColumn) {
1681
+ const filter = this.table.resolveIdFilter(data);
1682
+ const row = filter ? await this.table.findOne({
1683
+ filter,
1684
+ controls: {}
1685
+ }) : null;
1686
+ if (row === null) return new HttpError(404);
1687
+ return new HttpError(409, {
1688
+ message: "version_mismatch",
1689
+ statusCode: 409,
1690
+ kind: "version_mismatch",
1691
+ currentVersion: row[versionColumn]
1692
+ });
1632
1693
  }
1633
1694
  /**
1634
1695
  * **DELETE /:id** — removes a single record by primary key.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/moost-db",
3
- "version": "0.1.83",
3
+ "version": "0.1.85",
4
4
  "description": "Generic database controller for Moost with Atscript.",
5
5
  "keywords": [
6
6
  "annotations",
@@ -58,7 +58,7 @@
58
58
  "@wooksjs/event-core": "^0.7.13",
59
59
  "@wooksjs/http-body": "^0.7.13",
60
60
  "moost": "^0.6.15",
61
- "@atscript/db": "^0.1.83"
61
+ "@atscript/db": "^0.1.85"
62
62
  },
63
63
  "scripts": {
64
64
  "postinstall": "asc -f dts",