@atscript/moost-db 0.1.151 → 0.1.153

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
@@ -85,6 +85,9 @@ const dbErrorCodeToStatus = {
85
85
  CAS_MISMATCH: 409,
86
86
  TX_WAIT_TIMEOUT: 503,
87
87
  SPACE_CLOSED: 503,
88
+ DEADLOCK: 503,
89
+ LOCK_TIMEOUT: 503,
90
+ SERIALIZATION_FAILURE: 503,
88
91
  BUCKET_TZ_UNAVAILABLE: 501
89
92
  };
90
93
  function transformValidationError(error, reply) {
@@ -168,6 +171,15 @@ var SortControlDto = class {
168
171
  (0, _atscript_typescript_utils.throwFeatureDisabled)("JSON Schema", "jsonSchema", "emit.jsonSchema");
169
172
  }
170
173
  };
174
+ var NullsControlDto = class {
175
+ static __is_atscript_annotated_type = true;
176
+ static type = {};
177
+ static metadata = /* @__PURE__ */ new Map();
178
+ static id = "NullsControlDto";
179
+ static toJsonSchema() {
180
+ (0, _atscript_typescript_utils.throwFeatureDisabled)("JSON Schema", "jsonSchema", "emit.jsonSchema");
181
+ }
182
+ };
171
183
  var SelectControlDto = class {
172
184
  static __is_atscript_annotated_type = true;
173
185
  static type = {};
@@ -177,7 +189,7 @@ var SelectControlDto = class {
177
189
  (0, _atscript_typescript_utils.throwFeatureDisabled)("JSON Schema", "jsonSchema", "emit.jsonSchema");
178
190
  }
179
191
  };
180
- (0, _atscript_typescript_utils.defineAnnotatedType)("object", QueryControlsDto).prop("$skip", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$count", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$search", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$index", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$fuzzy", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$vector", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$threshold", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type).prop("$actions", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type);
192
+ (0, _atscript_typescript_utils.defineAnnotatedType)("object", QueryControlsDto).prop("$skip", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$count", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$nulls", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(NullsControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$search", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$index", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$fuzzy", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$vector", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$threshold", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type).prop("$actions", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type);
181
193
  (0, _atscript_typescript_utils.defineAnnotatedType)("object", PagesControlsDto).prop("$page", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").annotate("expect.pattern", {
182
194
  pattern: "^\\d+$",
183
195
  flags: "u",
@@ -186,7 +198,7 @@ var SelectControlDto = class {
186
198
  pattern: "^\\d+$",
187
199
  flags: "u",
188
200
  message: "Expected positive number"
189
- }, true).optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$search", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$index", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$fuzzy", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$vector", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$threshold", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type).prop("$actions", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type);
201
+ }, true).optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$nulls", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(NullsControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$search", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$index", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$fuzzy", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$vector", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$threshold", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type).prop("$actions", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type);
190
202
  (0, _atscript_typescript_utils.defineAnnotatedType)("object", GetOneControlsDto).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type).prop("$actions", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type);
191
203
  (0, _atscript_typescript_utils.defineAnnotatedType)("object", GeoControlsDto).prop("$center", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("number").$type).$type).optional().$type).prop("$maxDistance", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("number").optional().$type).prop("$minDistance", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("number").optional().$type).prop("$index", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$skip", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$page", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").annotate("expect.pattern", {
192
204
  pattern: "^\\d+$",
@@ -198,9 +210,10 @@ var SelectControlDto = class {
198
210
  message: "Expected positive number"
199
211
  }, true).optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type).prop("$actions", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type);
200
212
  (0, _atscript_typescript_utils.defineAnnotatedType)("object", WithRelationDto).prop("name", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).prop("filter", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithFilterDto).optional().$type).prop("controls", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationControlsDto).optional().$type).prop("insights", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithFilterDto).optional().$type);
201
- (0, _atscript_typescript_utils.defineAnnotatedType)("object", WithRelationControlsDto).prop("$skip", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type);
213
+ (0, _atscript_typescript_utils.defineAnnotatedType)("object", WithRelationControlsDto).prop("$skip", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$nulls", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(NullsControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type);
202
214
  (0, _atscript_typescript_utils.defineAnnotatedType)("object", WithFilterDto).propPattern(/./, (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("number").$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("null").tags("null").$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithFilterDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithFilterDto).$type).$type).$type);
203
215
  (0, _atscript_typescript_utils.defineAnnotatedType)("object", SortControlDto).propPattern(/./, (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").value(1).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").value(-1).$type).$type);
216
+ (0, _atscript_typescript_utils.defineAnnotatedType)("object", NullsControlDto).propPattern(/./, (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").value("first").$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").value("last").$type).$type);
204
217
  (0, _atscript_typescript_utils.defineAnnotatedType)("object", SelectControlDto).propPattern(/./, (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").value(1).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").value(0).$type).$type);
205
218
  //#endregion
206
219
  //#region src/mate.ts
@@ -2861,7 +2874,7 @@ let AsReadableController = _AsReadableController = class AsReadableController {
2861
2874
  * keys the shared db validator plugin reads in db-client (`db.json`,
2862
2875
  * `db.patch.strategy`, `db.default*`, `db.column.version`,
2863
2876
  * `db.column.derived`) and the client-facing `db.http.path` /
2864
- * `db.writeOnly`; strips every other `db.*` (table, column, index, etc.).
2877
+ * `db.writeOnly` / `db.sort.nulls`; strips every other `db.*` (table, column, index, etc.).
2865
2878
  * Override in subclass to customise — keep the validator keys, or client
2866
2879
  * preflight diverges from the server.
2867
2880
  */
@@ -2873,7 +2886,7 @@ let AsReadableController = _AsReadableController = class AsReadableController {
2873
2886
  key,
2874
2887
  value
2875
2888
  };
2876
- if (key === "db.json" || key === "db.patch.strategy" || key.startsWith("db.default") || key === "db.http.path" || key === "db.writeOnly" || key === "db.column.version" || key === "db.column.derived") return {
2889
+ if (key === "db.json" || key === "db.patch.strategy" || key.startsWith("db.default") || key === "db.http.path" || key === "db.writeOnly" || key === "db.column.version" || key === "db.column.derived" || key === "db.sort.nulls") return {
2877
2890
  key,
2878
2891
  value
2879
2892
  };
@@ -3499,7 +3512,7 @@ function validateMultiId(body, source, maxIds = Infinity) {
3499
3512
  }
3500
3513
  function collectIdErrors(value, source, opts) {
3501
3514
  const pathPrefix = opts.path ?? "";
3502
- if (!isPlainObject$2(value)) return [{
3515
+ if (!isPlainObject$3(value)) return [{
3503
3516
  path: pathPrefix,
3504
3517
  message: "Expected JSON object for row identifier",
3505
3518
  details: []
@@ -3550,7 +3563,7 @@ function describe(value) {
3550
3563
  if (Array.isArray(value)) return "array";
3551
3564
  return typeof value;
3552
3565
  }
3553
- function isPlainObject$2(value) {
3566
+ function isPlainObject$3(value) {
3554
3567
  return typeof value === "object" && value !== null && !Array.isArray(value);
3555
3568
  }
3556
3569
  //#endregion
@@ -5338,7 +5351,10 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
5338
5351
  if (sealed.size === 0) return void 0;
5339
5352
  const refs = (0, _atscript_db.collectQueryPaths)({
5340
5353
  filter: rel.filter,
5341
- controls: { $sort: nested.$sort ?? rel.$sort }
5354
+ controls: {
5355
+ $sort: nested.$sort ?? rel.$sort,
5356
+ $nulls: nested.$nulls ?? rel.$nulls
5357
+ }
5342
5358
  });
5343
5359
  const filtered = refs.filter.find((ref) => (0, _atscript_db.selfOrAncestor)(ref.path, sealed) !== void 0)?.path;
5344
5360
  if (filtered !== void 0) return writeOnlyError(`${path}.${filtered}`, "filter");
@@ -7053,7 +7069,29 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
7053
7069
  })).data;
7054
7070
  }
7055
7071
  /**
7056
- * **GET /pages** — returns paginated records with metadata.
7072
+ * The `/pages` read without a `$sort` (since 0.1.153): ordered by the
7073
+ * primary key, ascending, so consecutive pages neither overlap nor skip
7074
+ * rows. A request with `$sort` keeps it (the core appends the primary key
7075
+ * as the tie-breaker); a source without a primary key (a view without
7076
+ * `@meta.id`) stays unordered. Only the plain read: text / vector search
7077
+ * keep their relevance order.
7078
+ */
7079
+ _pagesOrder(q) {
7080
+ const controls = q.controls;
7081
+ const sort = controls?.$sort;
7082
+ const pk = this.readable.primaryKeys;
7083
+ if (sort && Object.keys(sort).length > 0 || pk.length === 0) return q;
7084
+ return {
7085
+ ...q,
7086
+ controls: {
7087
+ ...controls,
7088
+ $sort: Object.fromEntries(pk.map((f) => [f, 1]))
7089
+ }
7090
+ };
7091
+ }
7092
+ /**
7093
+ * **GET /pages** — returns paginated records with metadata. Without a
7094
+ * `$sort` the rows come in primary-key order (since 0.1.153).
7057
7095
  */
7058
7096
  async pages(url) {
7059
7097
  const { parsed, controls } = await this.parseRequest("pages", url);
@@ -7080,7 +7118,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
7080
7118
  switch (strategy.kind) {
7081
7119
  case "vector": return strategy.vectorField ? this.readable.vectorSearchWithCount(strategy.vectorField, strategy.vector, q) : this.readable.vectorSearchWithCount(strategy.vector, q);
7082
7120
  case "search": return this.readable.searchWithCount(strategy.term, q, strategy.index);
7083
- case "plain": return this.readable.findManyWithCount(q);
7121
+ case "plain": return this.readable.findManyWithCount(this._pagesOrder(q));
7084
7122
  }
7085
7123
  });
7086
7124
  return {
@@ -7564,7 +7602,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
7564
7602
  versionColumn: this.readable.versionColumn,
7565
7603
  ...capabilities.bucketUnits.length > 0 && { bucketUnits: [...capabilities.bucketUnits] },
7566
7604
  aggregateFns: [...capabilities.aggregateFns],
7567
- aggregateExpressions: capabilities.aggregateExpressions
7605
+ aggregateExpressions: capabilities.aggregateExpressions,
7606
+ ...typeof this.readable.supportsNullsPlacement === "function" && this.readable.supportsNullsPlacement() && { nullsPlacement: true }
7568
7607
  };
7569
7608
  }
7570
7609
  buildCrud() {
@@ -8164,7 +8203,22 @@ let AsValueHelpController = class AsValueHelpController extends AsReadableContro
8164
8203
  return (await this.query(await this._scopedQuery(parsed.filter, controls))).data;
8165
8204
  }
8166
8205
  /**
8167
- * **GET /pages** — paginated row window plus total count.
8206
+ * `/pages` controls without a `$sort` (since 0.1.153): ordered by the
8207
+ * primary key, ascending, so consecutive pages neither overlap nor skip
8208
+ * rows — like the DB controllers' `/pages`. A request with `$sort` or
8209
+ * `$search` keeps its order; a source without a primary key stays as is.
8210
+ */
8211
+ _pagesOrder(controls) {
8212
+ const sort = controls.$sort;
8213
+ if ((Array.isArray(sort) ? sort.length > 0 : (0, _atscript_db.isPlainObject)(sort) ? !(0, _atscript_db.isEmptyObject)(sort) : !!sort) || controls.$search || !this.primaryKey) return controls;
8214
+ return {
8215
+ ...controls,
8216
+ $sort: { [this.primaryKey]: 1 }
8217
+ };
8218
+ }
8219
+ /**
8220
+ * **GET /pages** — paginated row window plus total count. Without a
8221
+ * `$sort` the rows come in primary-key order (since 0.1.153).
8168
8222
  */
8169
8223
  async runPages(url) {
8170
8224
  const { parsed, controls } = await this.parseRequest("pages", url);
@@ -8173,11 +8227,11 @@ let AsValueHelpController = class AsValueHelpController extends AsReadableContro
8173
8227
  const page = Math.max(Number(controls.$page || 1), 1);
8174
8228
  const size = Math.max(Number(controls.$size || 10), 1);
8175
8229
  const skip = (page - 1) * size;
8176
- const result = await this.query(await this._scopedQuery(parsed.filter, {
8230
+ const result = await this.query(await this._scopedQuery(parsed.filter, this._pagesOrder({
8177
8231
  ...controls,
8178
8232
  $skip: skip,
8179
8233
  $limit: size
8180
- }));
8234
+ })));
8181
8235
  return {
8182
8236
  data: result.data,
8183
8237
  page,
@@ -8317,7 +8371,7 @@ let AsJsonValueHelpController = class AsJsonValueHelpController extends AsValueH
8317
8371
  rows = rows.filter((row) => predicate(row));
8318
8372
  }
8319
8373
  const sort = this.normalizeSort(controls.controls.$sort);
8320
- if (sort) rows = (0, _atscript_db_memory.sortRows)(rows, sort);
8374
+ if (sort) rows = (0, _atscript_db_memory.sortRows)(rows, sort, void 0, void 0, controls.controls.$nulls);
8321
8375
  const total = rows.length;
8322
8376
  const skip = Math.max(0, Number(controls.controls.$skip ?? 0));
8323
8377
  const limit = Math.max(0, Number(controls.controls.$limit ?? total - skip));
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as resolveDbSpace, i as provideDbSpace, n as clearDbSpaces, r as closeDbSpaces, t as DEFAULT_DB_SPACE } from "./db-space-registry-fXB8LnGe.cjs";
2
2
  import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, TSerializedAnnotatedType, TValidatorOptions, Validator } from "@atscript/typescript/utils";
3
- import { AggregateFn, AtscriptDbReadable, AtscriptDbTable, BucketUnit, FilterExpr, FlatOf, TCrudOp, TCrudOp as TCrudOp$1, TCrudPermissions, TCrudPermissions as TCrudPermissions$1, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionIntent as TDbActionIntent$1, TDbActionLevel, TDbActionLevel as TDbActionLevel$1, TDbActionProcessor, TDbActionTargetSummary, TDbActionTargetSummary as TDbActionTargetSummary$1, TDbAvailableActions, TDbAvailableActions as TDbAvailableActions$1, TDbFieldMeta, TDbRemoveGuardContext, TDbRemoveGuardContext as TDbRemoveGuardContext$1, TDbWriteAction, TDbWriteAction as TDbWriteAction$1, TDbWriteCheckContext, TDbWriteCheckContext as TDbWriteCheckContext$1, TDbWriteGuardContext, TDbWriteGuardContext as TDbWriteGuardContext$1, TFilterPredicate, TIdResolveOptions, TIdentification, TMetaResponse, TQueryPathOp, TQueryPathOp as TQueryPathOp$1, TQueryPathRefs, TQueryPathSource, Uniquery, UniqueryControls, collectQueryPaths } from "@atscript/db";
3
+ import { AggregateFn, AtscriptDbReadable, AtscriptDbTable, BucketUnit, FilterExpr, FlatOf, NullsPlacement, TCrudOp, TCrudOp as TCrudOp$1, TCrudPermissions, TCrudPermissions as TCrudPermissions$1, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionIntent as TDbActionIntent$1, TDbActionLevel, TDbActionLevel as TDbActionLevel$1, TDbActionProcessor, TDbActionTargetSummary, TDbActionTargetSummary as TDbActionTargetSummary$1, TDbAvailableActions, TDbAvailableActions as TDbAvailableActions$1, TDbFieldMeta, TDbRemoveGuardContext, TDbRemoveGuardContext as TDbRemoveGuardContext$1, TDbWriteAction, TDbWriteAction as TDbWriteAction$1, TDbWriteCheckContext, TDbWriteCheckContext as TDbWriteCheckContext$1, TDbWriteGuardContext, TDbWriteGuardContext as TDbWriteGuardContext$1, TFilterPredicate, TIdResolveOptions, TIdentification, TMetaResponse, TQueryPathOp, TQueryPathOp as TQueryPathOp$1, TQueryPathRefs, TQueryPathSource, Uniquery, UniqueryControls, collectQueryPaths } from "@atscript/db";
4
4
  import { HttpError } from "@moostjs/event-http";
5
5
  import { Mate, Moost, TConsoleBase, TMateParamMeta, TMoostMetadata } from "moost";
6
6
  import { parseUrl } from "@uniqu/url";
@@ -243,7 +243,7 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
243
243
  * keys the shared db validator plugin reads in db-client (`db.json`,
244
244
  * `db.patch.strategy`, `db.default*`, `db.column.version`,
245
245
  * `db.column.derived`) and the client-facing `db.http.path` /
246
- * `db.writeOnly`; strips every other `db.*` (table, column, index, etc.).
246
+ * `db.writeOnly` / `db.sort.nulls`; strips every other `db.*` (table, column, index, etc.).
247
247
  * Override in subclass to customise — keep the validator keys, or client
248
248
  * preflight diverges from the server.
249
249
  */
@@ -2076,7 +2076,17 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
2076
2076
  */
2077
2077
  query(url: string): Promise<DataType[] | number | HttpError>;
2078
2078
  /**
2079
- * **GET /pages** — returns paginated records with metadata.
2079
+ * The `/pages` read without a `$sort` (since 0.1.153): ordered by the
2080
+ * primary key, ascending, so consecutive pages neither overlap nor skip
2081
+ * rows. A request with `$sort` keeps it (the core appends the primary key
2082
+ * as the tie-breaker); a source without a primary key (a view without
2083
+ * `@meta.id`) stays unordered. Only the plain read: text / vector search
2084
+ * keep their relevance order.
2085
+ */
2086
+ private _pagesOrder;
2087
+ /**
2088
+ * **GET /pages** — returns paginated records with metadata. Without a
2089
+ * `$sort` the rows come in primary-key order (since 0.1.153).
2080
2090
  */
2081
2091
  pages(url: string): Promise<{
2082
2092
  data: DataType[];
@@ -2464,7 +2474,8 @@ interface ValueHelpQuery<T> {
2464
2474
  $limit?: number;
2465
2475
  $search?: string;
2466
2476
  $select?: (keyof T | string)[];
2467
- $sort?: unknown;
2477
+ $sort?: unknown; /** NULL placement per `$sort` key (URL `$sort=-name:last`; since 0.1.153). */
2478
+ $nulls?: Partial<Record<string, NullsPlacement>>;
2468
2479
  [key: string]: unknown;
2469
2480
  };
2470
2481
  }
@@ -2577,7 +2588,15 @@ declare abstract class AsValueHelpController<T extends TAtscriptAnnotatedType =
2577
2588
  */
2578
2589
  runQuery(url: string): Promise<DataType[] | HttpError>;
2579
2590
  /**
2580
- * **GET /pages** — paginated row window plus total count.
2591
+ * `/pages` controls without a `$sort` (since 0.1.153): ordered by the
2592
+ * primary key, ascending, so consecutive pages neither overlap nor skip
2593
+ * rows — like the DB controllers' `/pages`. A request with `$sort` or
2594
+ * `$search` keeps its order; a source without a primary key stays as is.
2595
+ */
2596
+ private _pagesOrder;
2597
+ /**
2598
+ * **GET /pages** — paginated row window plus total count. Without a
2599
+ * `$sort` the rows come in primary-key order (since 0.1.153).
2581
2600
  */
2582
2601
  runPages(url: string): Promise<{
2583
2602
  data: DataType[];
package/dist/index.d.mts CHANGED
@@ -3,7 +3,7 @@ import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, TSerializ
3
3
  import { HttpError, MoostHttp } from "@moostjs/event-http";
4
4
  import { Mate, Moost, TConsoleBase, TMateParamMeta, TMoostMetadata } from "moost";
5
5
  import { parseUrl } from "@uniqu/url";
6
- import { AggregateFn, AtscriptDbReadable, AtscriptDbTable, BucketUnit, FilterExpr, FlatOf, TCrudOp, TCrudOp as TCrudOp$1, TCrudPermissions, TCrudPermissions as TCrudPermissions$1, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionIntent as TDbActionIntent$1, TDbActionLevel, TDbActionLevel as TDbActionLevel$1, TDbActionProcessor, TDbActionTargetSummary, TDbActionTargetSummary as TDbActionTargetSummary$1, TDbAvailableActions, TDbAvailableActions as TDbAvailableActions$1, TDbFieldMeta, TDbRemoveGuardContext, TDbRemoveGuardContext as TDbRemoveGuardContext$1, TDbWriteAction, TDbWriteAction as TDbWriteAction$1, TDbWriteCheckContext, TDbWriteCheckContext as TDbWriteCheckContext$1, TDbWriteGuardContext, TDbWriteGuardContext as TDbWriteGuardContext$1, TFilterPredicate, TIdResolveOptions, TIdentification, TMetaResponse, TQueryPathOp, TQueryPathOp as TQueryPathOp$1, TQueryPathRefs, TQueryPathSource, Uniquery, UniqueryControls, collectQueryPaths } from "@atscript/db";
6
+ import { AggregateFn, AtscriptDbReadable, AtscriptDbTable, BucketUnit, FilterExpr, FlatOf, NullsPlacement, TCrudOp, TCrudOp as TCrudOp$1, TCrudPermissions, TCrudPermissions as TCrudPermissions$1, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionIntent as TDbActionIntent$1, TDbActionLevel, TDbActionLevel as TDbActionLevel$1, TDbActionProcessor, TDbActionTargetSummary, TDbActionTargetSummary as TDbActionTargetSummary$1, TDbAvailableActions, TDbAvailableActions as TDbAvailableActions$1, TDbFieldMeta, TDbRemoveGuardContext, TDbRemoveGuardContext as TDbRemoveGuardContext$1, TDbWriteAction, TDbWriteAction as TDbWriteAction$1, TDbWriteCheckContext, TDbWriteCheckContext as TDbWriteCheckContext$1, TDbWriteGuardContext, TDbWriteGuardContext as TDbWriteGuardContext$1, TFilterPredicate, TIdResolveOptions, TIdentification, TMetaResponse, TQueryPathOp, TQueryPathOp as TQueryPathOp$1, TQueryPathRefs, TQueryPathSource, Uniquery, UniqueryControls, collectQueryPaths } from "@atscript/db";
7
7
  import { EventContext } from "@wooksjs/event-core";
8
8
 
9
9
  //#region src/meta/meta-cache.d.ts
@@ -243,7 +243,7 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
243
243
  * keys the shared db validator plugin reads in db-client (`db.json`,
244
244
  * `db.patch.strategy`, `db.default*`, `db.column.version`,
245
245
  * `db.column.derived`) and the client-facing `db.http.path` /
246
- * `db.writeOnly`; strips every other `db.*` (table, column, index, etc.).
246
+ * `db.writeOnly` / `db.sort.nulls`; strips every other `db.*` (table, column, index, etc.).
247
247
  * Override in subclass to customise — keep the validator keys, or client
248
248
  * preflight diverges from the server.
249
249
  */
@@ -2076,7 +2076,17 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
2076
2076
  */
2077
2077
  query(url: string): Promise<DataType[] | number | HttpError>;
2078
2078
  /**
2079
- * **GET /pages** — returns paginated records with metadata.
2079
+ * The `/pages` read without a `$sort` (since 0.1.153): ordered by the
2080
+ * primary key, ascending, so consecutive pages neither overlap nor skip
2081
+ * rows. A request with `$sort` keeps it (the core appends the primary key
2082
+ * as the tie-breaker); a source without a primary key (a view without
2083
+ * `@meta.id`) stays unordered. Only the plain read: text / vector search
2084
+ * keep their relevance order.
2085
+ */
2086
+ private _pagesOrder;
2087
+ /**
2088
+ * **GET /pages** — returns paginated records with metadata. Without a
2089
+ * `$sort` the rows come in primary-key order (since 0.1.153).
2080
2090
  */
2081
2091
  pages(url: string): Promise<{
2082
2092
  data: DataType[];
@@ -2464,7 +2474,8 @@ interface ValueHelpQuery<T> {
2464
2474
  $limit?: number;
2465
2475
  $search?: string;
2466
2476
  $select?: (keyof T | string)[];
2467
- $sort?: unknown;
2477
+ $sort?: unknown; /** NULL placement per `$sort` key (URL `$sort=-name:last`; since 0.1.153). */
2478
+ $nulls?: Partial<Record<string, NullsPlacement>>;
2468
2479
  [key: string]: unknown;
2469
2480
  };
2470
2481
  }
@@ -2577,7 +2588,15 @@ declare abstract class AsValueHelpController<T extends TAtscriptAnnotatedType =
2577
2588
  */
2578
2589
  runQuery(url: string): Promise<DataType[] | HttpError>;
2579
2590
  /**
2580
- * **GET /pages** — paginated row window plus total count.
2591
+ * `/pages` controls without a `$sort` (since 0.1.153): ordered by the
2592
+ * primary key, ascending, so consecutive pages neither overlap nor skip
2593
+ * rows — like the DB controllers' `/pages`. A request with `$sort` or
2594
+ * `$search` keeps its order; a source without a primary key stays as is.
2595
+ */
2596
+ private _pagesOrder;
2597
+ /**
2598
+ * **GET /pages** — paginated row window plus total count. Without a
2599
+ * `$sort` the rows come in primary-key order (since 0.1.153).
2581
2600
  */
2582
2601
  runPages(url: string): Promise<{
2583
2602
  data: DataType[];
package/dist/index.mjs CHANGED
@@ -84,6 +84,9 @@ const dbErrorCodeToStatus = {
84
84
  CAS_MISMATCH: 409,
85
85
  TX_WAIT_TIMEOUT: 503,
86
86
  SPACE_CLOSED: 503,
87
+ DEADLOCK: 503,
88
+ LOCK_TIMEOUT: 503,
89
+ SERIALIZATION_FAILURE: 503,
87
90
  BUCKET_TZ_UNAVAILABLE: 501
88
91
  };
89
92
  function transformValidationError(error, reply) {
@@ -167,6 +170,15 @@ var SortControlDto = class {
167
170
  throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
168
171
  }
169
172
  };
173
+ var NullsControlDto = class {
174
+ static __is_atscript_annotated_type = true;
175
+ static type = {};
176
+ static metadata = /* @__PURE__ */ new Map();
177
+ static id = "NullsControlDto";
178
+ static toJsonSchema() {
179
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
180
+ }
181
+ };
170
182
  var SelectControlDto = class {
171
183
  static __is_atscript_annotated_type = true;
172
184
  static type = {};
@@ -176,7 +188,7 @@ var SelectControlDto = class {
176
188
  throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
177
189
  }
178
190
  };
179
- defineAnnotatedType("object", QueryControlsDto).prop("$skip", defineAnnotatedType().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", defineAnnotatedType().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$count", defineAnnotatedType().designType("boolean").tags("boolean").optional().$type).prop("$sort", defineAnnotatedType().refTo(SortControlDto).optional().$type).prop("$select", defineAnnotatedType("union").item(defineAnnotatedType().refTo(SelectControlDto).$type).item(defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).$type).optional().$type).prop("$search", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$index", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$fuzzy", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$vector", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$threshold", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$with", defineAnnotatedType("array").of(defineAnnotatedType().refTo(WithRelationDto).$type).optional().$type).prop("$actions", defineAnnotatedType().designType("boolean").tags("boolean").optional().$type);
191
+ defineAnnotatedType("object", QueryControlsDto).prop("$skip", defineAnnotatedType().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", defineAnnotatedType().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$count", defineAnnotatedType().designType("boolean").tags("boolean").optional().$type).prop("$sort", defineAnnotatedType().refTo(SortControlDto).optional().$type).prop("$nulls", defineAnnotatedType().refTo(NullsControlDto).optional().$type).prop("$select", defineAnnotatedType("union").item(defineAnnotatedType().refTo(SelectControlDto).$type).item(defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).$type).optional().$type).prop("$search", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$index", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$fuzzy", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$vector", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$threshold", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$with", defineAnnotatedType("array").of(defineAnnotatedType().refTo(WithRelationDto).$type).optional().$type).prop("$actions", defineAnnotatedType().designType("boolean").tags("boolean").optional().$type);
180
192
  defineAnnotatedType("object", PagesControlsDto).prop("$page", defineAnnotatedType().designType("string").tags("string").annotate("expect.pattern", {
181
193
  pattern: "^\\d+$",
182
194
  flags: "u",
@@ -185,7 +197,7 @@ defineAnnotatedType("object", PagesControlsDto).prop("$page", defineAnnotatedTyp
185
197
  pattern: "^\\d+$",
186
198
  flags: "u",
187
199
  message: "Expected positive number"
188
- }, true).optional().$type).prop("$sort", defineAnnotatedType().refTo(SortControlDto).optional().$type).prop("$select", defineAnnotatedType("union").item(defineAnnotatedType().refTo(SelectControlDto).$type).item(defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).$type).optional().$type).prop("$search", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$index", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$fuzzy", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$vector", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$threshold", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$with", defineAnnotatedType("array").of(defineAnnotatedType().refTo(WithRelationDto).$type).optional().$type).prop("$actions", defineAnnotatedType().designType("boolean").tags("boolean").optional().$type);
200
+ }, true).optional().$type).prop("$sort", defineAnnotatedType().refTo(SortControlDto).optional().$type).prop("$nulls", defineAnnotatedType().refTo(NullsControlDto).optional().$type).prop("$select", defineAnnotatedType("union").item(defineAnnotatedType().refTo(SelectControlDto).$type).item(defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).$type).optional().$type).prop("$search", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$index", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$fuzzy", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$vector", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$threshold", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$with", defineAnnotatedType("array").of(defineAnnotatedType().refTo(WithRelationDto).$type).optional().$type).prop("$actions", defineAnnotatedType().designType("boolean").tags("boolean").optional().$type);
189
201
  defineAnnotatedType("object", GetOneControlsDto).prop("$select", defineAnnotatedType("union").item(defineAnnotatedType().refTo(SelectControlDto).$type).item(defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).$type).optional().$type).prop("$with", defineAnnotatedType("array").of(defineAnnotatedType().refTo(WithRelationDto).$type).optional().$type).prop("$actions", defineAnnotatedType().designType("boolean").tags("boolean").optional().$type);
190
202
  defineAnnotatedType("object", GeoControlsDto).prop("$center", defineAnnotatedType("union").item(defineAnnotatedType().designType("string").tags("string").$type).item(defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).$type).item(defineAnnotatedType("array").of(defineAnnotatedType().designType("number").tags("number").$type).$type).optional().$type).prop("$maxDistance", defineAnnotatedType().designType("number").tags("number").optional().$type).prop("$minDistance", defineAnnotatedType().designType("number").tags("number").optional().$type).prop("$index", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("$select", defineAnnotatedType("union").item(defineAnnotatedType().refTo(SelectControlDto).$type).item(defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).$type).optional().$type).prop("$skip", defineAnnotatedType().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", defineAnnotatedType().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$page", defineAnnotatedType().designType("string").tags("string").annotate("expect.pattern", {
191
203
  pattern: "^\\d+$",
@@ -197,9 +209,10 @@ defineAnnotatedType("object", GeoControlsDto).prop("$center", defineAnnotatedTyp
197
209
  message: "Expected positive number"
198
210
  }, true).optional().$type).prop("$with", defineAnnotatedType("array").of(defineAnnotatedType().refTo(WithRelationDto).$type).optional().$type).prop("$actions", defineAnnotatedType().designType("boolean").tags("boolean").optional().$type);
199
211
  defineAnnotatedType("object", WithRelationDto).prop("name", defineAnnotatedType().designType("string").tags("string").$type).prop("filter", defineAnnotatedType().refTo(WithFilterDto).optional().$type).prop("controls", defineAnnotatedType().refTo(WithRelationControlsDto).optional().$type).prop("insights", defineAnnotatedType().refTo(WithFilterDto).optional().$type);
200
- defineAnnotatedType("object", WithRelationControlsDto).prop("$skip", defineAnnotatedType().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", defineAnnotatedType().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$sort", defineAnnotatedType().refTo(SortControlDto).optional().$type).prop("$select", defineAnnotatedType("union").item(defineAnnotatedType().refTo(SelectControlDto).$type).item(defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).$type).optional().$type).prop("$with", defineAnnotatedType("array").of(defineAnnotatedType().refTo(WithRelationDto).$type).optional().$type);
212
+ defineAnnotatedType("object", WithRelationControlsDto).prop("$skip", defineAnnotatedType().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", defineAnnotatedType().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$sort", defineAnnotatedType().refTo(SortControlDto).optional().$type).prop("$nulls", defineAnnotatedType().refTo(NullsControlDto).optional().$type).prop("$select", defineAnnotatedType("union").item(defineAnnotatedType().refTo(SelectControlDto).$type).item(defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).$type).optional().$type).prop("$with", defineAnnotatedType("array").of(defineAnnotatedType().refTo(WithRelationDto).$type).optional().$type);
201
213
  defineAnnotatedType("object", WithFilterDto).propPattern(/./, defineAnnotatedType("union").item(defineAnnotatedType().designType("string").tags("string").$type).item(defineAnnotatedType().designType("number").tags("number").$type).item(defineAnnotatedType().designType("boolean").tags("boolean").$type).item(defineAnnotatedType().designType("null").tags("null").$type).item(defineAnnotatedType().refTo(WithFilterDto).$type).item(defineAnnotatedType("array").of(defineAnnotatedType().refTo(WithFilterDto).$type).$type).$type);
202
214
  defineAnnotatedType("object", SortControlDto).propPattern(/./, defineAnnotatedType("union").item(defineAnnotatedType().designType("number").value(1).$type).item(defineAnnotatedType().designType("number").value(-1).$type).$type);
215
+ defineAnnotatedType("object", NullsControlDto).propPattern(/./, defineAnnotatedType("union").item(defineAnnotatedType().designType("string").value("first").$type).item(defineAnnotatedType().designType("string").value("last").$type).$type);
203
216
  defineAnnotatedType("object", SelectControlDto).propPattern(/./, defineAnnotatedType("union").item(defineAnnotatedType().designType("number").value(1).$type).item(defineAnnotatedType().designType("number").value(0).$type).$type);
204
217
  //#endregion
205
218
  //#region src/mate.ts
@@ -2860,7 +2873,7 @@ let AsReadableController = _AsReadableController = class AsReadableController {
2860
2873
  * keys the shared db validator plugin reads in db-client (`db.json`,
2861
2874
  * `db.patch.strategy`, `db.default*`, `db.column.version`,
2862
2875
  * `db.column.derived`) and the client-facing `db.http.path` /
2863
- * `db.writeOnly`; strips every other `db.*` (table, column, index, etc.).
2876
+ * `db.writeOnly` / `db.sort.nulls`; strips every other `db.*` (table, column, index, etc.).
2864
2877
  * Override in subclass to customise — keep the validator keys, or client
2865
2878
  * preflight diverges from the server.
2866
2879
  */
@@ -2872,7 +2885,7 @@ let AsReadableController = _AsReadableController = class AsReadableController {
2872
2885
  key,
2873
2886
  value
2874
2887
  };
2875
- if (key === "db.json" || key === "db.patch.strategy" || key.startsWith("db.default") || key === "db.http.path" || key === "db.writeOnly" || key === "db.column.version" || key === "db.column.derived") return {
2888
+ if (key === "db.json" || key === "db.patch.strategy" || key.startsWith("db.default") || key === "db.http.path" || key === "db.writeOnly" || key === "db.column.version" || key === "db.column.derived" || key === "db.sort.nulls") return {
2876
2889
  key,
2877
2890
  value
2878
2891
  };
@@ -5335,7 +5348,10 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
5335
5348
  if (sealed.size === 0) return void 0;
5336
5349
  const refs = collectQueryPaths$1({
5337
5350
  filter: rel.filter,
5338
- controls: { $sort: nested.$sort ?? rel.$sort }
5351
+ controls: {
5352
+ $sort: nested.$sort ?? rel.$sort,
5353
+ $nulls: nested.$nulls ?? rel.$nulls
5354
+ }
5339
5355
  });
5340
5356
  const filtered = refs.filter.find((ref) => selfOrAncestor(ref.path, sealed) !== void 0)?.path;
5341
5357
  if (filtered !== void 0) return writeOnlyError(`${path}.${filtered}`, "filter");
@@ -7050,7 +7066,29 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
7050
7066
  })).data;
7051
7067
  }
7052
7068
  /**
7053
- * **GET /pages** — returns paginated records with metadata.
7069
+ * The `/pages` read without a `$sort` (since 0.1.153): ordered by the
7070
+ * primary key, ascending, so consecutive pages neither overlap nor skip
7071
+ * rows. A request with `$sort` keeps it (the core appends the primary key
7072
+ * as the tie-breaker); a source without a primary key (a view without
7073
+ * `@meta.id`) stays unordered. Only the plain read: text / vector search
7074
+ * keep their relevance order.
7075
+ */
7076
+ _pagesOrder(q) {
7077
+ const controls = q.controls;
7078
+ const sort = controls?.$sort;
7079
+ const pk = this.readable.primaryKeys;
7080
+ if (sort && Object.keys(sort).length > 0 || pk.length === 0) return q;
7081
+ return {
7082
+ ...q,
7083
+ controls: {
7084
+ ...controls,
7085
+ $sort: Object.fromEntries(pk.map((f) => [f, 1]))
7086
+ }
7087
+ };
7088
+ }
7089
+ /**
7090
+ * **GET /pages** — returns paginated records with metadata. Without a
7091
+ * `$sort` the rows come in primary-key order (since 0.1.153).
7054
7092
  */
7055
7093
  async pages(url) {
7056
7094
  const { parsed, controls } = await this.parseRequest("pages", url);
@@ -7077,7 +7115,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
7077
7115
  switch (strategy.kind) {
7078
7116
  case "vector": return strategy.vectorField ? this.readable.vectorSearchWithCount(strategy.vectorField, strategy.vector, q) : this.readable.vectorSearchWithCount(strategy.vector, q);
7079
7117
  case "search": return this.readable.searchWithCount(strategy.term, q, strategy.index);
7080
- case "plain": return this.readable.findManyWithCount(q);
7118
+ case "plain": return this.readable.findManyWithCount(this._pagesOrder(q));
7081
7119
  }
7082
7120
  });
7083
7121
  return {
@@ -7561,7 +7599,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
7561
7599
  versionColumn: this.readable.versionColumn,
7562
7600
  ...capabilities.bucketUnits.length > 0 && { bucketUnits: [...capabilities.bucketUnits] },
7563
7601
  aggregateFns: [...capabilities.aggregateFns],
7564
- aggregateExpressions: capabilities.aggregateExpressions
7602
+ aggregateExpressions: capabilities.aggregateExpressions,
7603
+ ...typeof this.readable.supportsNullsPlacement === "function" && this.readable.supportsNullsPlacement() && { nullsPlacement: true }
7565
7604
  };
7566
7605
  }
7567
7606
  buildCrud() {
@@ -8161,7 +8200,22 @@ let AsValueHelpController = class AsValueHelpController extends AsReadableContro
8161
8200
  return (await this.query(await this._scopedQuery(parsed.filter, controls))).data;
8162
8201
  }
8163
8202
  /**
8164
- * **GET /pages** — paginated row window plus total count.
8203
+ * `/pages` controls without a `$sort` (since 0.1.153): ordered by the
8204
+ * primary key, ascending, so consecutive pages neither overlap nor skip
8205
+ * rows — like the DB controllers' `/pages`. A request with `$sort` or
8206
+ * `$search` keeps its order; a source without a primary key stays as is.
8207
+ */
8208
+ _pagesOrder(controls) {
8209
+ const sort = controls.$sort;
8210
+ if ((Array.isArray(sort) ? sort.length > 0 : isPlainObject(sort) ? !isEmptyObject(sort) : !!sort) || controls.$search || !this.primaryKey) return controls;
8211
+ return {
8212
+ ...controls,
8213
+ $sort: { [this.primaryKey]: 1 }
8214
+ };
8215
+ }
8216
+ /**
8217
+ * **GET /pages** — paginated row window plus total count. Without a
8218
+ * `$sort` the rows come in primary-key order (since 0.1.153).
8165
8219
  */
8166
8220
  async runPages(url) {
8167
8221
  const { parsed, controls } = await this.parseRequest("pages", url);
@@ -8170,11 +8224,11 @@ let AsValueHelpController = class AsValueHelpController extends AsReadableContro
8170
8224
  const page = Math.max(Number(controls.$page || 1), 1);
8171
8225
  const size = Math.max(Number(controls.$size || 10), 1);
8172
8226
  const skip = (page - 1) * size;
8173
- const result = await this.query(await this._scopedQuery(parsed.filter, {
8227
+ const result = await this.query(await this._scopedQuery(parsed.filter, this._pagesOrder({
8174
8228
  ...controls,
8175
8229
  $skip: skip,
8176
8230
  $limit: size
8177
- }));
8231
+ })));
8178
8232
  return {
8179
8233
  data: result.data,
8180
8234
  page,
@@ -8314,7 +8368,7 @@ let AsJsonValueHelpController = class AsJsonValueHelpController extends AsValueH
8314
8368
  rows = rows.filter((row) => predicate(row));
8315
8369
  }
8316
8370
  const sort = this.normalizeSort(controls.controls.$sort);
8317
- if (sort) rows = sortRows(rows, sort);
8371
+ if (sort) rows = sortRows(rows, sort, void 0, void 0, controls.controls.$nulls);
8318
8372
  const total = rows.length;
8319
8373
  const skip = Math.max(0, Number(controls.controls.$skip ?? 0));
8320
8374
  const limit = Math.max(0, Number(controls.controls.$limit ?? total - skip));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/moost-db",
3
- "version": "0.1.151",
3
+ "version": "0.1.153",
4
4
  "description": "Generic database controller for Moost with Atscript.",
5
5
  "keywords": [
6
6
  "annotations",
@@ -43,30 +43,30 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@uniqu/url": "^0.1.14",
47
- "@atscript/db-memory": "^0.1.151"
46
+ "@uniqu/url": "^0.1.15",
47
+ "@atscript/db-memory": "^0.1.153"
48
48
  },
49
49
  "devDependencies": {
50
- "@atscript/core": "^0.1.102",
51
- "@atscript/typescript": "^0.1.102",
52
- "@moostjs/event-http": "^0.6.48",
53
- "@uniqu/core": "^0.1.14",
54
- "@wooksjs/event-core": "^0.7.27",
55
- "@wooksjs/event-http": "^0.7.27",
56
- "@wooksjs/http-body": "^0.7.27",
50
+ "@atscript/core": "^0.1.103",
51
+ "@atscript/typescript": "^0.1.103",
52
+ "@moostjs/event-http": "^0.6.49",
53
+ "@uniqu/core": "^0.1.15",
54
+ "@wooksjs/event-core": "^0.7.28",
55
+ "@wooksjs/event-http": "^0.7.28",
56
+ "@wooksjs/http-body": "^0.7.28",
57
57
  "better-sqlite3": "^12.6.2",
58
- "moost": "^0.6.48",
59
- "unplugin-atscript": "^0.1.102",
60
- "@atscript/db-sqlite": "^0.1.151"
58
+ "moost": "^0.6.49",
59
+ "unplugin-atscript": "^0.1.103",
60
+ "@atscript/db-sqlite": "^0.1.153"
61
61
  },
62
62
  "peerDependencies": {
63
- "@atscript/typescript": "^0.1.102",
64
- "@moostjs/event-http": "^0.6.48",
65
- "@uniqu/core": "^0.1.14",
66
- "@wooksjs/event-core": "^0.7.27",
67
- "@wooksjs/http-body": "^0.7.27",
68
- "moost": "^0.6.48",
69
- "@atscript/db": "^0.1.151"
63
+ "@atscript/typescript": "^0.1.103",
64
+ "@moostjs/event-http": "^0.6.49",
65
+ "@uniqu/core": "^0.1.15",
66
+ "@wooksjs/event-core": "^0.7.28",
67
+ "@wooksjs/http-body": "^0.7.28",
68
+ "moost": "^0.6.49",
69
+ "@atscript/db": "^0.1.153"
70
70
  },
71
71
  "scripts": {
72
72
  "postinstall": "asc -f dts",