@objectstack/objectql 9.10.0 → 10.0.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/dist/index.d.mts +13 -6
- package/dist/index.d.ts +13 -6
- package/dist/index.js +224 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +224 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
package/dist/index.mjs
CHANGED
|
@@ -3870,7 +3870,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3870
3870
|
["$skip", "skip"],
|
|
3871
3871
|
["$orderby", "orderBy"],
|
|
3872
3872
|
["$select", "select"],
|
|
3873
|
-
["$count", "count"]
|
|
3873
|
+
["$count", "count"],
|
|
3874
|
+
["$search", "search"],
|
|
3875
|
+
["$searchFields", "searchFields"]
|
|
3874
3876
|
]) {
|
|
3875
3877
|
if (options[dollar] != null && options[bare] == null) {
|
|
3876
3878
|
options[bare] = options[dollar];
|
|
@@ -3968,6 +3970,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3968
3970
|
"aggregations",
|
|
3969
3971
|
"groupBy",
|
|
3970
3972
|
"search",
|
|
3973
|
+
"searchFields",
|
|
3971
3974
|
"context",
|
|
3972
3975
|
"cursor"
|
|
3973
3976
|
]);
|
|
@@ -6133,6 +6136,7 @@ var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
|
|
|
6133
6136
|
|
|
6134
6137
|
// src/engine.ts
|
|
6135
6138
|
import { AsyncLocalStorage } from "async_hooks";
|
|
6139
|
+
import { parseAutonumberFormat, renderAutonumber, missingFieldValues } from "@objectstack/spec/data";
|
|
6136
6140
|
import { ExecutionContextSchema } from "@objectstack/spec/kernel";
|
|
6137
6141
|
import { createLogger } from "@objectstack/core";
|
|
6138
6142
|
import { CoreServiceName, StorageNameMapping } from "@objectstack/spec/system";
|
|
@@ -6161,6 +6165,118 @@ function collectSecretFields(schema) {
|
|
|
6161
6165
|
|
|
6162
6166
|
// src/engine.ts
|
|
6163
6167
|
import { pluralToSingular, ExternalWriteForbiddenError } from "@objectstack/spec/shared";
|
|
6168
|
+
|
|
6169
|
+
// src/search-filter.ts
|
|
6170
|
+
var TEXTUAL_TYPES = /* @__PURE__ */ new Set(["text", "email", "phone", "url", "autonumber", "textarea", "markdown"]);
|
|
6171
|
+
var ENUM_TYPES = /* @__PURE__ */ new Set(["select", "status"]);
|
|
6172
|
+
var EXCLUDED_FIELDS = /* @__PURE__ */ new Set([
|
|
6173
|
+
"id",
|
|
6174
|
+
"_id",
|
|
6175
|
+
"created",
|
|
6176
|
+
"modified",
|
|
6177
|
+
"created_at",
|
|
6178
|
+
"updated_at",
|
|
6179
|
+
"created_by",
|
|
6180
|
+
"updated_by",
|
|
6181
|
+
"owner_id",
|
|
6182
|
+
"organization_id",
|
|
6183
|
+
"space",
|
|
6184
|
+
"company_id"
|
|
6185
|
+
]);
|
|
6186
|
+
var EXCLUDED_TYPES = /* @__PURE__ */ new Set([
|
|
6187
|
+
"json",
|
|
6188
|
+
"object",
|
|
6189
|
+
"grid",
|
|
6190
|
+
"image",
|
|
6191
|
+
"file",
|
|
6192
|
+
"avatar",
|
|
6193
|
+
"vector",
|
|
6194
|
+
"location",
|
|
6195
|
+
"geometry",
|
|
6196
|
+
"secret",
|
|
6197
|
+
"password",
|
|
6198
|
+
"encrypted",
|
|
6199
|
+
"boolean",
|
|
6200
|
+
"lookup",
|
|
6201
|
+
"master_detail"
|
|
6202
|
+
]);
|
|
6203
|
+
function normalizeSearch(raw) {
|
|
6204
|
+
if (raw == null) return { query: "" };
|
|
6205
|
+
if (typeof raw === "string") return { query: raw };
|
|
6206
|
+
if (typeof raw === "object") {
|
|
6207
|
+
const o = raw;
|
|
6208
|
+
const q = typeof o.query === "string" ? o.query : typeof o.q === "string" ? o.q : "";
|
|
6209
|
+
const fields = Array.isArray(o.fields) ? o.fields : void 0;
|
|
6210
|
+
return { query: q, fields };
|
|
6211
|
+
}
|
|
6212
|
+
return { query: "" };
|
|
6213
|
+
}
|
|
6214
|
+
function autoDefaultFields(fields, displayField) {
|
|
6215
|
+
const names = Object.keys(fields).filter((f) => {
|
|
6216
|
+
if (EXCLUDED_FIELDS.has(f)) return false;
|
|
6217
|
+
const meta = fields[f];
|
|
6218
|
+
if (!meta || meta.hidden) return false;
|
|
6219
|
+
const t = meta.type;
|
|
6220
|
+
if (!t) return false;
|
|
6221
|
+
if (EXCLUDED_TYPES.has(t)) return false;
|
|
6222
|
+
return TEXTUAL_TYPES.has(t) || ENUM_TYPES.has(t);
|
|
6223
|
+
});
|
|
6224
|
+
const lead = displayField && fields[displayField] ? displayField : fields.name ? "name" : fields.title ? "title" : void 0;
|
|
6225
|
+
if (!lead) return names;
|
|
6226
|
+
return [lead, ...names.filter((f) => f !== lead)];
|
|
6227
|
+
}
|
|
6228
|
+
function resolveSearchFields(opts) {
|
|
6229
|
+
const all = opts.fields || {};
|
|
6230
|
+
const declared = opts.searchableFields?.filter((f) => all[f]);
|
|
6231
|
+
const allowed = declared && declared.length > 0 ? declared : autoDefaultFields(all, opts.displayField);
|
|
6232
|
+
const requested = typeof opts.requestedFields === "string" ? opts.requestedFields.split(",").map((f) => f.trim()).filter(Boolean) : opts.requestedFields;
|
|
6233
|
+
if (requested && requested.length > 0) {
|
|
6234
|
+
const allowSet = new Set(allowed);
|
|
6235
|
+
const validated = requested.filter((f) => allowSet.has(f));
|
|
6236
|
+
if (validated.length > 0) return validated;
|
|
6237
|
+
}
|
|
6238
|
+
return allowed;
|
|
6239
|
+
}
|
|
6240
|
+
function optionValuesMatching(meta, term) {
|
|
6241
|
+
if (!Array.isArray(meta.options)) return [];
|
|
6242
|
+
const lc = term.toLowerCase();
|
|
6243
|
+
const out = [];
|
|
6244
|
+
for (const opt of meta.options) {
|
|
6245
|
+
if (opt == null) continue;
|
|
6246
|
+
if (typeof opt === "string") {
|
|
6247
|
+
if (opt.toLowerCase().includes(lc)) out.push(opt);
|
|
6248
|
+
continue;
|
|
6249
|
+
}
|
|
6250
|
+
const label = String(opt.label ?? opt.value ?? "");
|
|
6251
|
+
if (label.toLowerCase().includes(lc)) out.push(opt.value);
|
|
6252
|
+
}
|
|
6253
|
+
return out;
|
|
6254
|
+
}
|
|
6255
|
+
function fieldClausesForTerm(field, term, meta) {
|
|
6256
|
+
if (ENUM_TYPES.has(meta?.type ?? "")) {
|
|
6257
|
+
const values = optionValuesMatching(meta, term);
|
|
6258
|
+
if (values.length > 0) return [{ [field]: { $in: values } }];
|
|
6259
|
+
return [{ [field]: { $contains: term } }];
|
|
6260
|
+
}
|
|
6261
|
+
return [{ [field]: { $contains: term } }];
|
|
6262
|
+
}
|
|
6263
|
+
function expandSearchToFilter(raw, opts) {
|
|
6264
|
+
const { query, fields: requested } = normalizeSearch(raw);
|
|
6265
|
+
if (!query || !query.trim()) return null;
|
|
6266
|
+
const searchFields = resolveSearchFields({
|
|
6267
|
+
...opts,
|
|
6268
|
+
requestedFields: requested ?? opts.requestedFields
|
|
6269
|
+
});
|
|
6270
|
+
if (searchFields.length === 0) return null;
|
|
6271
|
+
const terms = query.trim().split(/\s+/).filter(Boolean);
|
|
6272
|
+
const andClauses = terms.map((term) => ({
|
|
6273
|
+
$or: searchFields.flatMap((f) => fieldClausesForTerm(f, term, opts.fields[f] || {}))
|
|
6274
|
+
}));
|
|
6275
|
+
if (andClauses.length === 0) return null;
|
|
6276
|
+
return andClauses.length === 1 ? andClauses[0] : { $and: andClauses };
|
|
6277
|
+
}
|
|
6278
|
+
|
|
6279
|
+
// src/engine.ts
|
|
6164
6280
|
import { ExpressionEngine as ExpressionEngine3 } from "@objectstack/formula";
|
|
6165
6281
|
import { isAggregatedViewContainer as isAggregatedViewContainer2, expandViewContainer } from "@objectstack/spec";
|
|
6166
6282
|
|
|
@@ -7558,8 +7674,9 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7558
7674
|
const tx = execCtx?.transaction !== void 0 ? execCtx.transaction : this.txStore.getStore()?.transaction;
|
|
7559
7675
|
const hasTx = tx !== void 0;
|
|
7560
7676
|
const hasTenant = execCtx?.tenantId !== void 0;
|
|
7677
|
+
const hasTz = execCtx?.timezone !== void 0;
|
|
7561
7678
|
const isSystem = execCtx?.isSystem === true;
|
|
7562
|
-
if (!hasTx && !hasTenant && !isSystem) return base;
|
|
7679
|
+
if (!hasTx && !hasTenant && !isSystem && !hasTz) return base;
|
|
7563
7680
|
const opts = base && typeof base === "object" ? { ...base } : {};
|
|
7564
7681
|
if (hasTx && opts.transaction === void 0) {
|
|
7565
7682
|
opts.transaction = tx;
|
|
@@ -7567,6 +7684,9 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7567
7684
|
if (hasTenant && opts.tenantId === void 0) {
|
|
7568
7685
|
opts.tenantId = execCtx.tenantId;
|
|
7569
7686
|
}
|
|
7687
|
+
if (hasTz && opts.timezone === void 0) {
|
|
7688
|
+
opts.timezone = execCtx.timezone;
|
|
7689
|
+
}
|
|
7570
7690
|
if (isSystem && opts.bypassTenantAudit === void 0) {
|
|
7571
7691
|
opts.bypassTenantAudit = true;
|
|
7572
7692
|
}
|
|
@@ -7640,29 +7760,48 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7640
7760
|
* owns the value, not the client.
|
|
7641
7761
|
*
|
|
7642
7762
|
* In the fallback path the next value is `max(existing) + 1`, seeded once per
|
|
7643
|
-
* `object.field
|
|
7644
|
-
* the process, resilient to deletions). `autonumberFormat`
|
|
7645
|
-
*
|
|
7763
|
+
* `object.field.<scope>` from the store then incremented in memory (monotonic
|
|
7764
|
+
* within the process, resilient to deletions). The shared `autonumberFormat`
|
|
7765
|
+
* renderer is honored end-to-end, so date tokens (`AD{YYYYMMDD}{0000}`), field
|
|
7766
|
+
* interpolation (`{island_zone}{000}`) and per-scope reset behave identically
|
|
7767
|
+
* to the SQL driver's persistent sequence (#1603). NOTE: this in-memory seeding
|
|
7768
|
+
* is single-instance.
|
|
7646
7769
|
*/
|
|
7647
7770
|
async applyAutonumbers(object, record, execCtx, driverOwnsAutonumber) {
|
|
7648
7771
|
if (driverOwnsAutonumber) return;
|
|
7649
7772
|
const fields = this.getSchema(object)?.fields;
|
|
7650
7773
|
if (!fields || typeof fields !== "object" || Array.isArray(fields)) return;
|
|
7774
|
+
const now = /* @__PURE__ */ new Date();
|
|
7775
|
+
const timezone = execCtx?.timezone;
|
|
7651
7776
|
for (const [name, def] of Object.entries(fields)) {
|
|
7652
7777
|
if (def?.type !== "autonumber") continue;
|
|
7653
7778
|
const current = record[name];
|
|
7654
7779
|
if (current != null && current !== "") continue;
|
|
7655
|
-
const key = `${object}.${name}`;
|
|
7656
|
-
let next = this.autonumberCounters.get(key);
|
|
7657
|
-
if (next == null) next = await this.seedAutonumber(object, name, execCtx);
|
|
7658
|
-
next += 1;
|
|
7659
|
-
this.autonumberCounters.set(key, next);
|
|
7660
7780
|
const fmt = def.autonumberFormat ?? def.format;
|
|
7661
|
-
|
|
7781
|
+
const tokens = parseAutonumberFormat(typeof fmt === "string" ? fmt : "");
|
|
7782
|
+
const missing = missingFieldValues(tokens, record);
|
|
7783
|
+
if (missing.length > 0) {
|
|
7784
|
+
throw new Error(
|
|
7785
|
+
`Cannot generate autonumber "${object}.${name}" (format "${fmt}"): referenced field(s) [${missing.join(", ")}] are empty on the record. Fields interpolated into an autonumber format must be set before the record is created.`
|
|
7786
|
+
);
|
|
7787
|
+
}
|
|
7788
|
+
const probe = renderAutonumber({ tokens, seq: 0, record, now, timezone });
|
|
7789
|
+
const counterKey = `${object}.${name}.${probe.scope}`;
|
|
7790
|
+
let next = this.autonumberCounters.get(counterKey);
|
|
7791
|
+
if (next == null) next = await this.seedAutonumber(object, name, probe.prefix, execCtx);
|
|
7792
|
+
next += 1;
|
|
7793
|
+
this.autonumberCounters.set(counterKey, next);
|
|
7794
|
+
record[name] = renderAutonumber({ tokens, seq: next, record, now, timezone }).value;
|
|
7662
7795
|
}
|
|
7663
7796
|
}
|
|
7664
|
-
/**
|
|
7665
|
-
|
|
7797
|
+
/**
|
|
7798
|
+
* Seed the autonumber counter from the current max in store, scoped to
|
|
7799
|
+
* `prefix`. With a non-empty prefix (date/field formats) only rows in the
|
|
7800
|
+
* same scope count, and the counter is the digit-run immediately after the
|
|
7801
|
+
* prefix; with an empty prefix (legacy fixed-prefix formats) the last digit
|
|
7802
|
+
* run of the whole value is used, preserving the original behaviour.
|
|
7803
|
+
*/
|
|
7804
|
+
async seedAutonumber(object, field, prefix, execCtx) {
|
|
7666
7805
|
try {
|
|
7667
7806
|
const rows = await this.find(object, {
|
|
7668
7807
|
select: ["id", field],
|
|
@@ -7673,22 +7812,24 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7673
7812
|
for (const r of rows || []) {
|
|
7674
7813
|
const v = r?.[field];
|
|
7675
7814
|
if (v == null) continue;
|
|
7676
|
-
const
|
|
7677
|
-
if (
|
|
7815
|
+
const s = String(v);
|
|
7816
|
+
if (prefix && !s.startsWith(prefix)) continue;
|
|
7817
|
+
const tail = prefix ? s.slice(prefix.length) : s;
|
|
7818
|
+
let digits;
|
|
7819
|
+
if (prefix) {
|
|
7820
|
+
const head = tail.match(/^\d+/);
|
|
7821
|
+
digits = head ? head[0] : void 0;
|
|
7822
|
+
} else {
|
|
7823
|
+
const runs = tail.match(/\d+/g);
|
|
7824
|
+
digits = runs ? runs[runs.length - 1] : void 0;
|
|
7825
|
+
}
|
|
7826
|
+
if (digits) max = Math.max(max, parseInt(digits, 10) || 0);
|
|
7678
7827
|
}
|
|
7679
7828
|
return max;
|
|
7680
7829
|
} catch {
|
|
7681
7830
|
return 0;
|
|
7682
7831
|
}
|
|
7683
7832
|
}
|
|
7684
|
-
/** Apply an autonumber format like `CASE-{0000}`; default to the bare number. */
|
|
7685
|
-
formatAutonumber(format, value) {
|
|
7686
|
-
if (!format) return String(value);
|
|
7687
|
-
const m = format.match(/\{(0+)\}/);
|
|
7688
|
-
if (!m) return format.includes("{0}") ? format.replace("{0}", String(value)) : `${format}${value}`;
|
|
7689
|
-
const padded = String(value).padStart(m[1].length, "0");
|
|
7690
|
-
return format.replace(m[0], padded);
|
|
7691
|
-
}
|
|
7692
7833
|
/**
|
|
7693
7834
|
* Register contribution (Manifest)
|
|
7694
7835
|
*
|
|
@@ -8449,11 +8590,18 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8449
8590
|
const uniqueIds = [...new Set(allIds)];
|
|
8450
8591
|
if (uniqueIds.length === 0) continue;
|
|
8451
8592
|
try {
|
|
8593
|
+
const idFilter = { id: { $in: uniqueIds } };
|
|
8594
|
+
const where = nestedAST.where ? { $and: [idFilter, nestedAST.where] } : idFilter;
|
|
8452
8595
|
const relatedQuery = {
|
|
8453
8596
|
object: referenceObject,
|
|
8454
|
-
where
|
|
8597
|
+
where,
|
|
8455
8598
|
...nestedAST.fields ? { fields: nestedAST.fields } : {},
|
|
8456
8599
|
...nestedAST.orderBy ? { orderBy: nestedAST.orderBy } : {}
|
|
8600
|
+
// NOTE: nestedAST.limit/offset are intentionally NOT forwarded here.
|
|
8601
|
+
// This path batch-loads every parent's related records in a single
|
|
8602
|
+
// $in query, so a *per-parent* limit/offset can't be expressed — a
|
|
8603
|
+
// global cap on the batch would silently drop records other parents
|
|
8604
|
+
// need. Paginate by querying the related object directly instead.
|
|
8457
8605
|
};
|
|
8458
8606
|
const driver = this.getDriver(referenceObject);
|
|
8459
8607
|
const expandOpts = this.buildDriverOptions(execCtx);
|
|
@@ -8506,11 +8654,34 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8506
8654
|
const driver = this.getDriver(object);
|
|
8507
8655
|
const ast = { object, ...query };
|
|
8508
8656
|
delete ast.context;
|
|
8657
|
+
if (ast.filter != null && ast.where == null) {
|
|
8658
|
+
ast.where = ast.filter;
|
|
8659
|
+
}
|
|
8660
|
+
delete ast.filter;
|
|
8509
8661
|
if (ast.top != null && ast.limit == null) {
|
|
8510
8662
|
ast.limit = ast.top;
|
|
8511
8663
|
}
|
|
8512
8664
|
delete ast.top;
|
|
8513
8665
|
const _findSchema = this._registry.getObject(object);
|
|
8666
|
+
{
|
|
8667
|
+
const _searchRaw = ast.search ?? ast.$search;
|
|
8668
|
+
if (_searchRaw != null && _findSchema?.fields) {
|
|
8669
|
+
const _reqFields = ast.searchFields ?? ast.$searchFields ?? (typeof ast.search === "object" ? ast.search?.fields : void 0);
|
|
8670
|
+
const _searchFilter = expandSearchToFilter(_searchRaw, {
|
|
8671
|
+
fields: _findSchema.fields,
|
|
8672
|
+
searchableFields: _findSchema.searchableFields,
|
|
8673
|
+
requestedFields: _reqFields,
|
|
8674
|
+
displayField: _findSchema.displayNameField
|
|
8675
|
+
});
|
|
8676
|
+
if (_searchFilter) {
|
|
8677
|
+
ast.where = ast.where ? { $and: [ast.where, _searchFilter] } : _searchFilter;
|
|
8678
|
+
}
|
|
8679
|
+
}
|
|
8680
|
+
delete ast.search;
|
|
8681
|
+
delete ast.$search;
|
|
8682
|
+
delete ast.searchFields;
|
|
8683
|
+
delete ast.$searchFields;
|
|
8684
|
+
}
|
|
8514
8685
|
const _findFormula = planFormulaProjection(_findSchema, ast.fields);
|
|
8515
8686
|
if (_findFormula.projected) ast.fields = _findFormula.projected;
|
|
8516
8687
|
if (_findSchema?.fields && Array.isArray(ast.fields) && ast.fields.length > 0) {
|
|
@@ -9215,7 +9386,14 @@ var _ObjectQL = class _ObjectQL {
|
|
|
9215
9386
|
const obj = this._registry.getObject(objectName);
|
|
9216
9387
|
if (!obj) return;
|
|
9217
9388
|
const driver = this.getDriverForObject(objectName);
|
|
9218
|
-
if (!driver
|
|
9389
|
+
if (!driver) return;
|
|
9390
|
+
if (obj.external != null) {
|
|
9391
|
+
if (typeof driver.registerExternalObject === "function") {
|
|
9392
|
+
await driver.registerExternalObject(obj);
|
|
9393
|
+
}
|
|
9394
|
+
return;
|
|
9395
|
+
}
|
|
9396
|
+
if (typeof driver.syncSchema !== "function") return;
|
|
9219
9397
|
const tableName = StorageNameMapping.resolveTableName(obj);
|
|
9220
9398
|
await driver.syncSchema(tableName, obj);
|
|
9221
9399
|
}
|
|
@@ -10049,6 +10227,27 @@ var ObjectQLPlugin = class {
|
|
|
10049
10227
|
skipped++;
|
|
10050
10228
|
continue;
|
|
10051
10229
|
}
|
|
10230
|
+
if (obj.external != null) {
|
|
10231
|
+
if (typeof driver.registerExternalObject === "function") {
|
|
10232
|
+
try {
|
|
10233
|
+
await driver.registerExternalObject(obj);
|
|
10234
|
+
synced++;
|
|
10235
|
+
} catch (e) {
|
|
10236
|
+
ctx.logger.warn("Failed to register external object metadata", {
|
|
10237
|
+
object: obj.name,
|
|
10238
|
+
driver: driver.name,
|
|
10239
|
+
error: e instanceof Error ? e.message : String(e)
|
|
10240
|
+
});
|
|
10241
|
+
}
|
|
10242
|
+
} else {
|
|
10243
|
+
ctx.logger.debug("Driver does not support registerExternalObject, skipping external object", {
|
|
10244
|
+
object: obj.name,
|
|
10245
|
+
driver: driver.name
|
|
10246
|
+
});
|
|
10247
|
+
skipped++;
|
|
10248
|
+
}
|
|
10249
|
+
continue;
|
|
10250
|
+
}
|
|
10052
10251
|
if (typeof driver.syncSchema !== "function") {
|
|
10053
10252
|
ctx.logger.debug("Driver does not support syncSchema, skipping", {
|
|
10054
10253
|
object: obj.name,
|