@objectstack/objectql 9.11.0 → 10.2.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.js CHANGED
@@ -3929,7 +3929,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3929
3929
  ["$skip", "skip"],
3930
3930
  ["$orderby", "orderBy"],
3931
3931
  ["$select", "select"],
3932
- ["$count", "count"]
3932
+ ["$count", "count"],
3933
+ ["$search", "search"],
3934
+ ["$searchFields", "searchFields"]
3933
3935
  ]) {
3934
3936
  if (options[dollar] != null && options[bare] == null) {
3935
3937
  options[bare] = options[dollar];
@@ -4027,6 +4029,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4027
4029
  "aggregations",
4028
4030
  "groupBy",
4029
4031
  "search",
4032
+ "searchFields",
4030
4033
  "context",
4031
4034
  "cursor"
4032
4035
  ]);
@@ -6221,6 +6224,118 @@ function collectSecretFields(schema) {
6221
6224
 
6222
6225
  // src/engine.ts
6223
6226
  var import_shared5 = require("@objectstack/spec/shared");
6227
+
6228
+ // src/search-filter.ts
6229
+ var TEXTUAL_TYPES = /* @__PURE__ */ new Set(["text", "email", "phone", "url", "autonumber", "textarea", "markdown"]);
6230
+ var ENUM_TYPES = /* @__PURE__ */ new Set(["select", "status"]);
6231
+ var EXCLUDED_FIELDS = /* @__PURE__ */ new Set([
6232
+ "id",
6233
+ "_id",
6234
+ "created",
6235
+ "modified",
6236
+ "created_at",
6237
+ "updated_at",
6238
+ "created_by",
6239
+ "updated_by",
6240
+ "owner_id",
6241
+ "organization_id",
6242
+ "space",
6243
+ "company_id"
6244
+ ]);
6245
+ var EXCLUDED_TYPES = /* @__PURE__ */ new Set([
6246
+ "json",
6247
+ "object",
6248
+ "grid",
6249
+ "image",
6250
+ "file",
6251
+ "avatar",
6252
+ "vector",
6253
+ "location",
6254
+ "geometry",
6255
+ "secret",
6256
+ "password",
6257
+ "encrypted",
6258
+ "boolean",
6259
+ "lookup",
6260
+ "master_detail"
6261
+ ]);
6262
+ function normalizeSearch(raw) {
6263
+ if (raw == null) return { query: "" };
6264
+ if (typeof raw === "string") return { query: raw };
6265
+ if (typeof raw === "object") {
6266
+ const o = raw;
6267
+ const q = typeof o.query === "string" ? o.query : typeof o.q === "string" ? o.q : "";
6268
+ const fields = Array.isArray(o.fields) ? o.fields : void 0;
6269
+ return { query: q, fields };
6270
+ }
6271
+ return { query: "" };
6272
+ }
6273
+ function autoDefaultFields(fields, displayField) {
6274
+ const names = Object.keys(fields).filter((f) => {
6275
+ if (EXCLUDED_FIELDS.has(f)) return false;
6276
+ const meta = fields[f];
6277
+ if (!meta || meta.hidden) return false;
6278
+ const t = meta.type;
6279
+ if (!t) return false;
6280
+ if (EXCLUDED_TYPES.has(t)) return false;
6281
+ return TEXTUAL_TYPES.has(t) || ENUM_TYPES.has(t);
6282
+ });
6283
+ const lead = displayField && fields[displayField] ? displayField : fields.name ? "name" : fields.title ? "title" : void 0;
6284
+ if (!lead) return names;
6285
+ return [lead, ...names.filter((f) => f !== lead)];
6286
+ }
6287
+ function resolveSearchFields(opts) {
6288
+ const all = opts.fields || {};
6289
+ const declared = opts.searchableFields?.filter((f) => all[f]);
6290
+ const allowed = declared && declared.length > 0 ? declared : autoDefaultFields(all, opts.displayField);
6291
+ const requested = typeof opts.requestedFields === "string" ? opts.requestedFields.split(",").map((f) => f.trim()).filter(Boolean) : opts.requestedFields;
6292
+ if (requested && requested.length > 0) {
6293
+ const allowSet = new Set(allowed);
6294
+ const validated = requested.filter((f) => allowSet.has(f));
6295
+ if (validated.length > 0) return validated;
6296
+ }
6297
+ return allowed;
6298
+ }
6299
+ function optionValuesMatching(meta, term) {
6300
+ if (!Array.isArray(meta.options)) return [];
6301
+ const lc = term.toLowerCase();
6302
+ const out = [];
6303
+ for (const opt of meta.options) {
6304
+ if (opt == null) continue;
6305
+ if (typeof opt === "string") {
6306
+ if (opt.toLowerCase().includes(lc)) out.push(opt);
6307
+ continue;
6308
+ }
6309
+ const label = String(opt.label ?? opt.value ?? "");
6310
+ if (label.toLowerCase().includes(lc)) out.push(opt.value);
6311
+ }
6312
+ return out;
6313
+ }
6314
+ function fieldClausesForTerm(field, term, meta) {
6315
+ if (ENUM_TYPES.has(meta?.type ?? "")) {
6316
+ const values = optionValuesMatching(meta, term);
6317
+ if (values.length > 0) return [{ [field]: { $in: values } }];
6318
+ return [{ [field]: { $contains: term } }];
6319
+ }
6320
+ return [{ [field]: { $contains: term } }];
6321
+ }
6322
+ function expandSearchToFilter(raw, opts) {
6323
+ const { query, fields: requested } = normalizeSearch(raw);
6324
+ if (!query || !query.trim()) return null;
6325
+ const searchFields = resolveSearchFields({
6326
+ ...opts,
6327
+ requestedFields: requested ?? opts.requestedFields
6328
+ });
6329
+ if (searchFields.length === 0) return null;
6330
+ const terms = query.trim().split(/\s+/).filter(Boolean);
6331
+ const andClauses = terms.map((term) => ({
6332
+ $or: searchFields.flatMap((f) => fieldClausesForTerm(f, term, opts.fields[f] || {}))
6333
+ }));
6334
+ if (andClauses.length === 0) return null;
6335
+ return andClauses.length === 1 ? andClauses[0] : { $and: andClauses };
6336
+ }
6337
+
6338
+ // src/engine.ts
6224
6339
  var import_formula4 = require("@objectstack/formula");
6225
6340
  var import_spec = require("@objectstack/spec");
6226
6341
 
@@ -8534,11 +8649,18 @@ var _ObjectQL = class _ObjectQL {
8534
8649
  const uniqueIds = [...new Set(allIds)];
8535
8650
  if (uniqueIds.length === 0) continue;
8536
8651
  try {
8652
+ const idFilter = { id: { $in: uniqueIds } };
8653
+ const where = nestedAST.where ? { $and: [idFilter, nestedAST.where] } : idFilter;
8537
8654
  const relatedQuery = {
8538
8655
  object: referenceObject,
8539
- where: { id: { $in: uniqueIds } },
8656
+ where,
8540
8657
  ...nestedAST.fields ? { fields: nestedAST.fields } : {},
8541
8658
  ...nestedAST.orderBy ? { orderBy: nestedAST.orderBy } : {}
8659
+ // NOTE: nestedAST.limit/offset are intentionally NOT forwarded here.
8660
+ // This path batch-loads every parent's related records in a single
8661
+ // $in query, so a *per-parent* limit/offset can't be expressed — a
8662
+ // global cap on the batch would silently drop records other parents
8663
+ // need. Paginate by querying the related object directly instead.
8542
8664
  };
8543
8665
  const driver = this.getDriver(referenceObject);
8544
8666
  const expandOpts = this.buildDriverOptions(execCtx);
@@ -8591,11 +8713,34 @@ var _ObjectQL = class _ObjectQL {
8591
8713
  const driver = this.getDriver(object);
8592
8714
  const ast = { object, ...query };
8593
8715
  delete ast.context;
8716
+ if (ast.filter != null && ast.where == null) {
8717
+ ast.where = ast.filter;
8718
+ }
8719
+ delete ast.filter;
8594
8720
  if (ast.top != null && ast.limit == null) {
8595
8721
  ast.limit = ast.top;
8596
8722
  }
8597
8723
  delete ast.top;
8598
8724
  const _findSchema = this._registry.getObject(object);
8725
+ {
8726
+ const _searchRaw = ast.search ?? ast.$search;
8727
+ if (_searchRaw != null && _findSchema?.fields) {
8728
+ const _reqFields = ast.searchFields ?? ast.$searchFields ?? (typeof ast.search === "object" ? ast.search?.fields : void 0);
8729
+ const _searchFilter = expandSearchToFilter(_searchRaw, {
8730
+ fields: _findSchema.fields,
8731
+ searchableFields: _findSchema.searchableFields,
8732
+ requestedFields: _reqFields,
8733
+ displayField: _findSchema.displayNameField
8734
+ });
8735
+ if (_searchFilter) {
8736
+ ast.where = ast.where ? { $and: [ast.where, _searchFilter] } : _searchFilter;
8737
+ }
8738
+ }
8739
+ delete ast.search;
8740
+ delete ast.$search;
8741
+ delete ast.searchFields;
8742
+ delete ast.$searchFields;
8743
+ }
8599
8744
  const _findFormula = planFormulaProjection(_findSchema, ast.fields);
8600
8745
  if (_findFormula.projected) ast.fields = _findFormula.projected;
8601
8746
  if (_findSchema?.fields && Array.isArray(ast.fields) && ast.fields.length > 0) {
@@ -9300,7 +9445,14 @@ var _ObjectQL = class _ObjectQL {
9300
9445
  const obj = this._registry.getObject(objectName);
9301
9446
  if (!obj) return;
9302
9447
  const driver = this.getDriverForObject(objectName);
9303
- if (!driver || typeof driver.syncSchema !== "function") return;
9448
+ if (!driver) return;
9449
+ if (obj.external != null) {
9450
+ if (typeof driver.registerExternalObject === "function") {
9451
+ await driver.registerExternalObject(obj);
9452
+ }
9453
+ return;
9454
+ }
9455
+ if (typeof driver.syncSchema !== "function") return;
9304
9456
  const tableName = import_system2.StorageNameMapping.resolveTableName(obj);
9305
9457
  await driver.syncSchema(tableName, obj);
9306
9458
  }
@@ -10129,6 +10281,27 @@ var ObjectQLPlugin = class {
10129
10281
  skipped++;
10130
10282
  continue;
10131
10283
  }
10284
+ if (obj.external != null) {
10285
+ if (typeof driver.registerExternalObject === "function") {
10286
+ try {
10287
+ await driver.registerExternalObject(obj);
10288
+ synced++;
10289
+ } catch (e) {
10290
+ ctx.logger.warn("Failed to register external object metadata", {
10291
+ object: obj.name,
10292
+ driver: driver.name,
10293
+ error: e instanceof Error ? e.message : String(e)
10294
+ });
10295
+ }
10296
+ } else {
10297
+ ctx.logger.debug("Driver does not support registerExternalObject, skipping external object", {
10298
+ object: obj.name,
10299
+ driver: driver.name
10300
+ });
10301
+ skipped++;
10302
+ }
10303
+ continue;
10304
+ }
10132
10305
  if (typeof driver.syncSchema !== "function") {
10133
10306
  ctx.logger.debug("Driver does not support syncSchema, skipping", {
10134
10307
  object: obj.name,