@objectstack/objectql 9.11.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.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
  ]);
@@ -6162,6 +6165,118 @@ function collectSecretFields(schema) {
6162
6165
 
6163
6166
  // src/engine.ts
6164
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
6165
6280
  import { ExpressionEngine as ExpressionEngine3 } from "@objectstack/formula";
6166
6281
  import { isAggregatedViewContainer as isAggregatedViewContainer2, expandViewContainer } from "@objectstack/spec";
6167
6282
 
@@ -8475,11 +8590,18 @@ var _ObjectQL = class _ObjectQL {
8475
8590
  const uniqueIds = [...new Set(allIds)];
8476
8591
  if (uniqueIds.length === 0) continue;
8477
8592
  try {
8593
+ const idFilter = { id: { $in: uniqueIds } };
8594
+ const where = nestedAST.where ? { $and: [idFilter, nestedAST.where] } : idFilter;
8478
8595
  const relatedQuery = {
8479
8596
  object: referenceObject,
8480
- where: { id: { $in: uniqueIds } },
8597
+ where,
8481
8598
  ...nestedAST.fields ? { fields: nestedAST.fields } : {},
8482
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.
8483
8605
  };
8484
8606
  const driver = this.getDriver(referenceObject);
8485
8607
  const expandOpts = this.buildDriverOptions(execCtx);
@@ -8532,11 +8654,34 @@ var _ObjectQL = class _ObjectQL {
8532
8654
  const driver = this.getDriver(object);
8533
8655
  const ast = { object, ...query };
8534
8656
  delete ast.context;
8657
+ if (ast.filter != null && ast.where == null) {
8658
+ ast.where = ast.filter;
8659
+ }
8660
+ delete ast.filter;
8535
8661
  if (ast.top != null && ast.limit == null) {
8536
8662
  ast.limit = ast.top;
8537
8663
  }
8538
8664
  delete ast.top;
8539
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
+ }
8540
8685
  const _findFormula = planFormulaProjection(_findSchema, ast.fields);
8541
8686
  if (_findFormula.projected) ast.fields = _findFormula.projected;
8542
8687
  if (_findSchema?.fields && Array.isArray(ast.fields) && ast.fields.length > 0) {
@@ -9241,7 +9386,14 @@ var _ObjectQL = class _ObjectQL {
9241
9386
  const obj = this._registry.getObject(objectName);
9242
9387
  if (!obj) return;
9243
9388
  const driver = this.getDriverForObject(objectName);
9244
- if (!driver || typeof driver.syncSchema !== "function") return;
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;
9245
9397
  const tableName = StorageNameMapping.resolveTableName(obj);
9246
9398
  await driver.syncSchema(tableName, obj);
9247
9399
  }
@@ -10075,6 +10227,27 @@ var ObjectQLPlugin = class {
10075
10227
  skipped++;
10076
10228
  continue;
10077
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
+ }
10078
10251
  if (typeof driver.syncSchema !== "function") {
10079
10252
  ctx.logger.debug("Driver does not support syncSchema, skipping", {
10080
10253
  object: obj.name,