@open-mercato/core 0.6.6-develop.5834.1.dc9eb0615e → 0.6.6-develop.5900.1.b3fb52925e

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.
@@ -14,7 +14,9 @@ import {
14
14
  import { resolveSearchConfig } from "@open-mercato/shared/lib/search/config";
15
15
  import { tokenizeText } from "@open-mercato/shared/lib/search/tokenize";
16
16
  import { runBeforeQueryPipeline, runAfterQueryPipeline } from "@open-mercato/shared/lib/query/query-extension-runner";
17
- import { resolveEncryptedSortFields, sortRowsInMemory } from "@open-mercato/shared/lib/query/encrypted-sort";
17
+ import { resolveEncryptedSortFields, resolveEncryptedSortMaxRows, sortRowsInMemory } from "@open-mercato/shared/lib/query/encrypted-sort";
18
+ import { mapWithConcurrency } from "@open-mercato/shared/lib/query/bounded-decrypt";
19
+ const DECRYPT_CONCURRENCY = 8;
18
20
  function buildFilterableCustomFieldJoins(sources) {
19
21
  if (!sources || sources.length === 0) return [];
20
22
  return sources.flatMap((source, index) => {
@@ -662,9 +664,10 @@ class HybridQueryEngine {
662
664
  opts.includeCustomFields.map((key) => String(key)).forEach((key) => selectFieldSet.add(`cf:${key}`));
663
665
  }
664
666
  const selectFields = Array.from(selectFieldSet);
665
- const applySelection = (q) => {
667
+ const applySelection = (q, fieldsOverride) => {
666
668
  let next = q;
667
- for (const field of selectFields) {
669
+ const fields = fieldsOverride ?? selectFields;
670
+ for (const field of fields) {
668
671
  const fieldName = String(field);
669
672
  if (fieldName.startsWith("cf:")) {
670
673
  const alias = this.sanitize(fieldName);
@@ -751,71 +754,122 @@ class HybridQueryEngine {
751
754
  );
752
755
  total = this.parseCount(countRow);
753
756
  }
754
- const dataRoot = db.selectFrom(`${baseTable} as b`);
755
- let dataBuilder = await applyQueryShape(dataRoot);
756
- dataBuilder = applySelection(dataBuilder);
757
- dataBuilder = applySort(dataBuilder);
758
- if (!requiresPlaintextSort) {
759
- dataBuilder = dataBuilder.limit(pageSize).offset((page - 1) * pageSize);
760
- }
761
- if (debugEnabled && sqlDebugEnabled) {
762
- const compiled = dataBuilder.compile();
763
- this.debug("query:sql:data", { entity, sql: compiled.sql, bindings: compiled.parameters, page, pageSize });
764
- }
765
- const itemsRaw = await this.captureSqlTiming(
766
- "query:sql:data",
767
- entity,
768
- () => dataBuilder.execute(),
769
- { page, pageSize },
770
- profiler
771
- );
772
- if (debugEnabled) this.debug("query:complete", { entity, total, items: Array.isArray(itemsRaw) ? itemsRaw.length : 0 });
773
- let items = itemsRaw;
774
757
  const dekKeyCache = /* @__PURE__ */ new Map();
775
- if (encSvc?.decryptEntityPayload) {
776
- const decrypt = encSvc.decryptEntityPayload.bind(encSvc);
777
- items = await Promise.all(
778
- items.map(async (item) => {
779
- try {
780
- const decrypted = await decrypt(
781
- entity,
782
- item,
783
- item?.tenant_id ?? item?.tenantId ?? opts.tenantId ?? null,
784
- item?.organization_id ?? item?.organizationId ?? fallbackOrgId ?? null
785
- );
786
- return { ...item, ...decrypted };
787
- } catch (err) {
788
- console.error("Error decrypting entity payload", err);
789
- return item;
790
- }
791
- })
758
+ const decryptRow = async (item) => {
759
+ let next = item;
760
+ if (encSvc?.decryptEntityPayload) {
761
+ const decrypt = encSvc.decryptEntityPayload.bind(encSvc);
762
+ try {
763
+ const decrypted = await decrypt(
764
+ entity,
765
+ next,
766
+ next?.tenant_id ?? next?.tenantId ?? opts.tenantId ?? null,
767
+ next?.organization_id ?? next?.organizationId ?? fallbackOrgId ?? null
768
+ );
769
+ next = { ...next, ...decrypted };
770
+ } catch (err) {
771
+ console.error("Error decrypting entity payload", err);
772
+ }
773
+ }
774
+ if (encSvc) {
775
+ try {
776
+ next = await decryptIndexDocCustomFields(
777
+ next,
778
+ {
779
+ tenantId: next?.tenant_id ?? next?.tenantId ?? opts.tenantId ?? null,
780
+ organizationId: next?.organization_id ?? next?.organizationId ?? null
781
+ },
782
+ encSvc,
783
+ dekKeyCache
784
+ );
785
+ } catch {
786
+ }
787
+ }
788
+ return next;
789
+ };
790
+ let items;
791
+ let encryptedSortRowCapWarning;
792
+ if (requiresPlaintextSort) {
793
+ const cap = resolveEncryptedSortMaxRows();
794
+ const phase1Root = db.selectFrom(`${baseTable} as b`);
795
+ let phase1 = await applyQueryShape(phase1Root);
796
+ const sortFieldNames = Array.from(/* @__PURE__ */ new Set(["id", ...resolvedSorts.map((s) => String(s.field))]));
797
+ phase1 = applySelection(phase1, sortFieldNames);
798
+ if (cap !== null) {
799
+ phase1 = phase1.limit(cap).orderBy(qualify("id"), "asc");
800
+ }
801
+ if (debugEnabled && sqlDebugEnabled) {
802
+ const compiled = phase1.compile();
803
+ this.debug("query:sql:data:phase1", { entity, sql: compiled.sql, bindings: compiled.parameters });
804
+ }
805
+ const candidateRows = await this.captureSqlTiming(
806
+ "query:sql:data:phase1",
807
+ entity,
808
+ () => phase1.execute(),
809
+ { phase: 1 },
810
+ profiler
792
811
  );
793
- }
794
- if (encSvc) {
795
- items = await Promise.all(
796
- items.map(async (item) => {
797
- try {
798
- return await decryptIndexDocCustomFields(
799
- item,
800
- {
801
- tenantId: item?.tenant_id ?? item?.tenantId ?? opts.tenantId ?? null,
802
- organizationId: item?.organization_id ?? item?.organizationId ?? null
803
- },
804
- encSvc,
805
- dekKeyCache
806
- );
807
- } catch {
808
- return item;
809
- }
810
- })
812
+ const decryptedCandidates = await mapWithConcurrency(candidateRows, DECRYPT_CONCURRENCY, decryptRow);
813
+ const orderedCandidates = sortRowsInMemory(decryptedCandidates, resolvedSorts);
814
+ const pageIds = orderedCandidates.slice((page - 1) * pageSize, page * pageSize).map((row) => row.id);
815
+ if (cap !== null && total > cap) {
816
+ encryptedSortRowCapWarning = {
817
+ entity,
818
+ sortFields: resolvedSorts.map((s) => String(s.field)),
819
+ maxRows: cap,
820
+ totalMatched: total
821
+ };
822
+ }
823
+ if (pageIds.length === 0) {
824
+ items = [];
825
+ } else {
826
+ const phase2Root = db.selectFrom(`${baseTable} as b`);
827
+ let phase2 = await applyQueryShape(phase2Root);
828
+ phase2 = applySelection(phase2);
829
+ phase2 = phase2.where(qualify("id"), "in", pageIds);
830
+ if (debugEnabled && sqlDebugEnabled) {
831
+ const compiled = phase2.compile();
832
+ this.debug("query:sql:data:phase2", { entity, sql: compiled.sql, bindings: compiled.parameters });
833
+ }
834
+ const pageRowsRaw = await this.captureSqlTiming(
835
+ "query:sql:data:phase2",
836
+ entity,
837
+ () => phase2.execute(),
838
+ { phase: 2 },
839
+ profiler
840
+ );
841
+ const decryptedPageRows = await mapWithConcurrency(pageRowsRaw, DECRYPT_CONCURRENCY, decryptRow);
842
+ const byId = new Map(decryptedPageRows.map((row) => [String(row.id), row]));
843
+ items = pageIds.map((id) => byId.get(String(id))).filter((row) => row != null);
844
+ }
845
+ } else {
846
+ const dataRoot = db.selectFrom(`${baseTable} as b`);
847
+ let dataBuilder = await applyQueryShape(dataRoot);
848
+ dataBuilder = applySelection(dataBuilder);
849
+ dataBuilder = applySort(dataBuilder);
850
+ dataBuilder = dataBuilder.limit(pageSize).offset((page - 1) * pageSize);
851
+ if (debugEnabled && sqlDebugEnabled) {
852
+ const compiled = dataBuilder.compile();
853
+ this.debug("query:sql:data", { entity, sql: compiled.sql, bindings: compiled.parameters, page, pageSize });
854
+ }
855
+ const itemsRaw = await this.captureSqlTiming(
856
+ "query:sql:data",
857
+ entity,
858
+ () => dataBuilder.execute(),
859
+ { page, pageSize },
860
+ profiler
811
861
  );
862
+ items = await mapWithConcurrency(itemsRaw, DECRYPT_CONCURRENCY, decryptRow);
812
863
  }
813
- if (requiresPlaintextSort) {
814
- items = sortRowsInMemory(items, resolvedSorts).slice((page - 1) * pageSize, page * pageSize);
815
- }
864
+ if (debugEnabled) this.debug("query:complete", { entity, total, items: items.length });
816
865
  const typedItems = items;
817
866
  let result = { items: typedItems, page, pageSize, total };
818
- if (partialIndexWarning) result.meta = { partialIndexWarning };
867
+ if (partialIndexWarning || encryptedSortRowCapWarning) {
868
+ const meta = {};
869
+ if (partialIndexWarning) meta.partialIndexWarning = partialIndexWarning;
870
+ if (encryptedSortRowCapWarning) meta.encryptedSortRowCapWarning = encryptedSortRowCapWarning;
871
+ result.meta = meta;
872
+ }
819
873
  result = await applyAfterExtensions(result);
820
874
  finishProfile({
821
875
  result: "ok",