@memberjunction/metadata-sync 5.37.0 → 5.38.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/README.md CHANGED
@@ -195,7 +195,48 @@ The tool is intended for managing business-level metadata such as:
195
195
  - Permissions and security settings
196
196
  - Any other data that is not reflected **up** from the underlying system database catalogs
197
197
 
198
- For more information about how CodeGen reflects system-level data from the database into the MJ metadata layer, see the [CodeGen documentation](../CodeGen/README.md).
198
+ For more information about how CodeGen reflects system-level data from the database into the MJ metadata layer, see the [CodeGen documentation](../CodeGenLib/README.md).
199
+
200
+ ## Performance: Preloading and Caching During Push
201
+
202
+ The `push` command preloads existing records and caches files/lookups so the
203
+ per-record sync work avoids redundant DB round-trips and disk I/O.
204
+
205
+ ### 1. Upfront bulk preload (batched `RunViews`)
206
+
207
+ Before processing records, the push scans every JSON file in the target
208
+ directories (in parallel, with a concurrency cap), collects primary keys
209
+ from records and nested `relatedEntities`, groups them by entity, and
210
+ issues a single filtered `RunView` per entity. This replaces the
211
+ prior per-record existence check with one batched read per entity at
212
+ push start.
213
+
214
+ ### 2. Event-driven in-memory cache (`SyncMetadataEngine` extends `BaseEngine`)
215
+
216
+ `SyncMetadataEngine` plugs into MJ's `BaseEntity` event bus via `BaseEngine`.
217
+ Saves and deletes performed during the push update the in-memory cache in
218
+ place, so subsequent lookups inside the same run see the new state without
219
+ a re-fetch. The engine overrides `canUseImmediateMutation` to allow this
220
+ in-place update for its own preload configs — the filter is built from
221
+ the preloaded PKs, so updates and deletes stay safe, and newly-created
222
+ records are deliberately added to the cache so later lookups can find them
223
+ within the same push.
224
+
225
+ ### 3. File and lookup caches
226
+
227
+ - **File cache**: parsed JSON and resolved `@include` output are cached on
228
+ first read so the validation and sync passes don't reparse. The cache
229
+ entry for a file is invalidated whenever the push writes that file back
230
+ to disk.
231
+ - **Lookup cache**: resolved `@lookup` (and `@parent` / `@root`) values are
232
+ memoized per `(entity, lookup-fields)` key. The cache is indexed by
233
+ entity so deleting a record invalidates only the affected entries
234
+ instead of clearing the whole cache. Keys are URI-encoded to prevent
235
+ collisions when user data contains `=` / `&` / `|`.
236
+
237
+ > Real-world reductions vary with the size and shape of your metadata
238
+ > tree; expect the biggest wins on syncs that read many records of the
239
+ > same entity (which is the common case).
199
240
 
200
241
  ## File Structure
201
242
 
@@ -1849,8 +1890,8 @@ Processing AI Prompts in demo/ai-prompts
1849
1890
  ## Dependencies
1850
1891
 
1851
1892
  This package depends on:
1852
- - [@memberjunction/core](../MJCore/README.md) -- Core MemberJunction framework (Metadata, RunView, BaseEntity)
1853
- - [@memberjunction/core-entities](../MJCoreEntities/README.md) -- Generated entity subclasses
1893
+ - [@memberjunction/core](../MJCore/readme.md) -- Core MemberJunction framework (Metadata, RunView, BaseEntity)
1894
+ - [@memberjunction/core-entities](../MJCoreEntities/readme.md) -- Generated entity subclasses
1854
1895
  - [@memberjunction/global](../MJGlobal/README.md) -- Global utilities and class factory
1855
1896
  - [@memberjunction/config](../Config/README.md) -- Configuration management
1856
1897
  - [@memberjunction/sqlserver-dataprovider](../SQLServerDataProvider/README.md) -- SQL Server data access
@@ -1887,3 +1928,104 @@ If you were previously using the standalone `mj-sync` command:
1887
1928
  ## Contributing
1888
1929
 
1889
1930
  See the [MemberJunction Contributing Guide](../../CONTRIBUTING.md) for development setup and guidelines.
1931
+
1932
+ ---
1933
+
1934
+ ## Notes — Push Performance Optimizations (v5.38.x)
1935
+
1936
+ The `push` path was substantially reworked to eliminate per-record DB
1937
+ round-trips on metadata trees with thousands of records. End-to-end
1938
+ measurements on a representative ~36,500-record `metadata/` tree (mostly
1939
+ idempotent, including a `metadata/integrations/` dir with 23,789 records):
1940
+
1941
+ | Scenario | Pre-optimization | Post-optimization | Speedup |
1942
+ |---|---:|---:|---:|
1943
+ | Full sync (incl. integrations) | ~6m 49s | **~1m 4s** | **~6.5×** |
1944
+ | Partial sync (excluding integrations) | ~1m 37s | **~30.5s** | **~3.2×** |
1945
+
1946
+ The changes sit entirely inside the `push` flow; behaviour, configuration,
1947
+ and on-disk formats are unchanged.
1948
+
1949
+ ### What changed
1950
+
1951
+ 1. **Upfront preloading via `SyncMetadataEngine`** (extends `BaseEngine`).
1952
+ Before processing any file the engine scans every JSON file under each
1953
+ configured entity directory once, collects the set of entity names
1954
+ referenced (including nested `relatedEntities`), and issues a single
1955
+ unfiltered `RunView` per entity through `BaseEngine.Load`. The
1956
+ resulting `BaseEntity` instances live on dynamic per-entity property
1957
+ slots that the sync path consults instead of round-tripping the DB
1958
+ per record.
1959
+
1960
+ Why unfiltered: metadata entities (Actions, Prompts, Agents,
1961
+ Templates, …) are bounded by design. Loading all rows once is faster
1962
+ than computing a giant `WHERE … IN (…)` clause, and it lets
1963
+ `@lookup:` resolution hit the cache even for records that are not
1964
+ directly mentioned in local files. An oversize warning fires if any
1965
+ single entity comes back with more than 100,000 rows so operators
1966
+ notice when something non-metadata-scale slips into a sync workflow.
1967
+
1968
+ 2. **O(1) PK index on the preload cache.** Each preloaded slot is also
1969
+ indexed into a per-entity `Map<serializedPK, BaseEntity>`. The
1970
+ sync's `loadEntity` path uses this for O(1) hash lookups instead of
1971
+ the previous `Array.find(... serializePrimaryKey(GetAll()))` scan.
1972
+
1973
+ This was the single biggest fix. The naïve `Array.find` was O(N×K)
1974
+ where N = cached rows and K = records being processed. For
1975
+ `MJ: Integration Object Fields` (~100K+ DB rows, ~23,789 records
1976
+ processed) that was ~1.2B comparisons → ~38 minutes. After the Map
1977
+ index the integrations dir alone dropped from 38 minutes to seconds.
1978
+ The index has a self-healing array-scan fallback for entries
1979
+ introduced by `BaseEngine`'s event-driven slot mutations.
1980
+
1981
+ 3. **Resolved-lookup cache + cached file reads.** Resolved `@lookup:`
1982
+ keys are memoized in a per-engine `Map<lookupKey, ID>`, indexed by
1983
+ entity so the cache invalidates only the affected entries on delete
1984
+ instead of clearing wholesale. The parsed + `@include`-preprocessed
1985
+ contents of every file are also cached, so validation and sync
1986
+ passes don't re-read or re-parse the same file twice. File-cache
1987
+ entries are invalidated at every write site (immediate and deferred
1988
+ writes) so later passes within the same push see fresh contents.
1989
+
1990
+ 4. **Skip preload for unresolved PK references.** Records whose
1991
+ `primaryKey` still carries an unresolved `@lookup:` / `@parent:` /
1992
+ `@root:` / `@file:` / `@env:` / `@template:` reference are skipped
1993
+ at the preload step — `SyncEngine`'s per-record path resolves them
1994
+ later. Without this guard the preload would inline literal
1995
+ `@lookup:…` strings into a `WHERE ID = '…'` filter and SQL Server
1996
+ would (correctly) reject the cast to uniqueidentifier.
1997
+
1998
+ 5. **Provider plumbing.** The push flow no longer reaches for
1999
+ `Metadata.Provider` directly during cache and lookup writes —
2000
+ `SyncEngine.getProvider()` is the single entry point. Single-process
2001
+ today; ready for multi-provider scenarios without further surgery.
2002
+
2003
+ ### Related core fix shipped at the same time
2004
+
2005
+ Fixed-width / space-padded character types (`nchar`/`char` on SQL Server;
2006
+ `char`/`character`/`bpchar` on PostgreSQL) used to surface their storage
2007
+ padding through `BaseEntity.Get`, causing dirty-check to compare e.g.
2008
+ `"Input "` against `"Input"` and false-positive every record as
2009
+ dirty. Once preload populated the in-memory comparison this manifested
2010
+ as thousands of spurious "updates" per sync (~4,279 on `MJ: Action
2011
+ Params` alone).
2012
+
2013
+ The fix is in `@memberjunction/core`:
2014
+
2015
+ - New `EntityFieldInfo.FixedWidthColumn` getter, delegating to a new
2016
+ `IsFixedWidthStringSQLType` predicate in `@memberjunction/sql-dialect`
2017
+ so the list of fixed-width type names stays in one place per dialect.
2018
+ - `EntityField.Value` setter and `BaseEntity.Get` raw fast-path now
2019
+ rtrim string values when `FixedWidthColumn` is true, memoizing back
2020
+ into `_raw` so the trim runs once per field per record.
2021
+
2022
+ This is independent of MetadataSync but was exposed by the preload work
2023
+ and is required for the "Unchanged" counts to be accurate.
2024
+
2025
+ ### Observability
2026
+
2027
+ `SyncMetadataEngine.drainWarnings()` exposes non-fatal warnings
2028
+ collected during preload (malformed `relatedEntities` shapes, oversized
2029
+ entities). `PushService` surfaces them through `callbacks.onWarn` so
2030
+ they appear in the CLI's standard warning channel alongside everything
2031
+ else.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import '@memberjunction/server-bootstrap-lite/mj-class-registrations';
2
2
  export { FileBackupManager } from './lib/file-backup-manager.js';
3
3
  export { SyncEngine, DeferrableLookupError } from './lib/sync-engine.js';
4
+ export { SyncMetadataEngine } from './lib/sync-metadata-engine.js';
4
5
  export type { RecordData, BatchContext } from './lib/sync-engine.js';
5
6
  export { BatchContextIndex } from './lib/batch-context-index.js';
6
7
  export type { BatchContextStub } from './lib/batch-context-index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import '@memberjunction/server-bootstrap-lite/mj-class-registrations';
3
3
  // Core library exports
4
4
  export { FileBackupManager } from './lib/file-backup-manager.js';
5
5
  export { SyncEngine, DeferrableLookupError } from './lib/sync-engine.js';
6
+ export { SyncMetadataEngine } from './lib/sync-metadata-engine.js';
6
7
  export { BatchContextIndex } from './lib/batch-context-index.js';
7
8
  export { ConfigManager, configManager } from './lib/config-manager.js';
8
9
  export { getSyncEngine, resetSyncEngine } from './lib/singleton-manager.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,OAAO,8DAA8D,CAAC;AAEtE,uBAAuB;AACvB,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAEtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAE9D,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAG5D,yBAAyB;AACzB,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AAEzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAE1E,kBAAkB;AAClB,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGrD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAGzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAG/D,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAGvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAEjE,sBAAsB;AACtB,OAAO,EACL,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAIjB,MAAM,UAAU,CAAC;AAElB,qBAAqB;AACrB,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,aAAa,EACb,qBAAqB,EACrB,eAAe,EAChB,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,OAAO,8DAA8D,CAAC;AAEtE,uBAAuB;AACvB,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAE9D,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAG5D,yBAAyB;AACzB,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AAEzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAE1E,kBAAkB;AAClB,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGrD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAGzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAG/D,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAGvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAEjE,sBAAsB;AACtB,OAAO,EACL,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAIjB,MAAM,UAAU,CAAC;AAElB,qBAAqB;AACrB,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,aAAa,EACb,qBAAqB,EACrB,eAAe,EAChB,MAAM,sBAAsB,CAAC"}
@@ -37,13 +37,21 @@ export declare class ConfigManager extends BaseSingleton<ConfigManager> {
37
37
  */
38
38
  setOriginalCwd(cwd: string): void;
39
39
  /**
40
- * Load MemberJunction configuration
40
+ * Load MemberJunction configuration.
41
41
  *
42
- * Searches for mj.config.cjs starting from the original working directory
43
- * and walking up the directory tree. Caches the result for subsequent calls.
42
+ * Resolution order:
43
+ * 1. `process.env.MJ_CONFIG_FILE` — if set to an absolute or repo-relative
44
+ * path of a config file (e.g. `mj.config.cjs`), load that file directly.
45
+ * Used by the regression archive flow to swap configs between
46
+ * `mj sync pull` (source DB) and `mj sync push` (archive DB) without
47
+ * touching the filesystem.
48
+ * 2. Otherwise: cosmiconfig search starting from the original working
49
+ * directory, walking up. This is the original behavior.
44
50
  *
45
- * @param forceReload - Force reload the configuration even if cached
46
- * @returns MJConfig object if found, null if not found or invalid
51
+ * The loaded user config is merged into `DEFAULT_SYNC_CONFIG` so env-var
52
+ * overrides for database settings continue to apply.
53
+ *
54
+ * Result is cached for subsequent calls; pass `forceReload=true` to invalidate.
47
55
  */
48
56
  loadMJConfig(forceReload?: boolean): MJConfig | null;
49
57
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"config-manager.d.ts","sourceRoot":"","sources":["../../src/lib/config-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AA+BrC;;;;;;GAMG;AACH,qBAAa,aAAc,SAAQ,aAAa,CAAC,aAAa,CAAC;IAC7D,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,YAAY,CAAS;;IAM7B;;OAEG;IACH,WAAkB,QAAQ,IAAI,aAAa,CAE1C;IAED;;;;OAIG;IACH,cAAc,IAAI,MAAM;IAQxB;;;;OAIG;IACH,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIjC;;;;;;;;OAQG;IACH,YAAY,CAAC,WAAW,UAAQ,GAAG,QAAQ,GAAG,IAAI;IA0BlD;;;;OAIG;IACH,WAAW,IAAI,QAAQ,GAAG,IAAI;CAM/B;AAGD,eAAO,MAAM,aAAa,eAAyB,CAAC"}
1
+ {"version":3,"file":"config-manager.d.ts","sourceRoot":"","sources":["../../src/lib/config-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAiCrC;;;;;;GAMG;AACH,qBAAa,aAAc,SAAQ,aAAa,CAAC,aAAa,CAAC;IAC7D,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,YAAY,CAAS;;IAM7B;;OAEG;IACH,WAAkB,QAAQ,IAAI,aAAa,CAE1C;IAED;;;;OAIG;IACH,cAAc,IAAI,MAAM;IAQxB;;;;OAIG;IACH,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIjC;;;;;;;;;;;;;;;;OAgBG;IACH,YAAY,CAAC,WAAW,UAAQ,GAAG,QAAQ,GAAG,IAAI;IAyClD;;;;OAIG;IACH,WAAW,IAAI,QAAQ,GAAG,IAAI;CAM/B;AAGD,eAAO,MAAM,aAAa,eAAyB,CAAC"}
@@ -10,6 +10,8 @@ import { cosmiconfigSync } from 'cosmiconfig';
10
10
  import { mergeConfigs, parseBooleanEnv } from '@memberjunction/config';
11
11
  import { BaseSingleton } from '@memberjunction/global';
12
12
  import { resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
13
+ import * as fs from 'fs';
14
+ import * as path from 'path';
13
15
  const ENV_DB_PLATFORM = resolveDbPlatformFromEnv();
14
16
  /**
15
17
  * Default configuration for MetadataSync
@@ -78,13 +80,21 @@ export class ConfigManager extends BaseSingleton {
78
80
  this.originalCwd = cwd;
79
81
  }
80
82
  /**
81
- * Load MemberJunction configuration
83
+ * Load MemberJunction configuration.
82
84
  *
83
- * Searches for mj.config.cjs starting from the original working directory
84
- * and walking up the directory tree. Caches the result for subsequent calls.
85
+ * Resolution order:
86
+ * 1. `process.env.MJ_CONFIG_FILE` — if set to an absolute or repo-relative
87
+ * path of a config file (e.g. `mj.config.cjs`), load that file directly.
88
+ * Used by the regression archive flow to swap configs between
89
+ * `mj sync pull` (source DB) and `mj sync push` (archive DB) without
90
+ * touching the filesystem.
91
+ * 2. Otherwise: cosmiconfig search starting from the original working
92
+ * directory, walking up. This is the original behavior.
85
93
  *
86
- * @param forceReload - Force reload the configuration even if cached
87
- * @returns MJConfig object if found, null if not found or invalid
94
+ * The loaded user config is merged into `DEFAULT_SYNC_CONFIG` so env-var
95
+ * overrides for database settings continue to apply.
96
+ *
97
+ * Result is cached for subsequent calls; pass `forceReload=true` to invalidate.
88
98
  */
89
99
  loadMJConfig(forceReload = false) {
90
100
  if (this.configLoaded && !forceReload) {
@@ -92,9 +102,25 @@ export class ConfigManager extends BaseSingleton {
92
102
  }
93
103
  try {
94
104
  const explorer = cosmiconfigSync('mj');
95
- // Always search from the original working directory
96
- const searchPath = this.getOriginalCwd();
97
- const result = explorer.search(searchPath);
105
+ let result;
106
+ const envOverride = process.env.MJ_CONFIG_FILE;
107
+ if (envOverride && envOverride.trim()) {
108
+ // Resolve relative to the original cwd so callers can pass either an
109
+ // absolute path or a path relative to where the user invoked the CLI.
110
+ const resolved = path.isAbsolute(envOverride)
111
+ ? envOverride
112
+ : path.resolve(this.getOriginalCwd(), envOverride);
113
+ if (!fs.existsSync(resolved)) {
114
+ console.error(`MJ_CONFIG_FILE points at "${resolved}" which does not exist; falling back to cosmiconfig search`);
115
+ result = explorer.search(this.getOriginalCwd());
116
+ }
117
+ else {
118
+ result = explorer.load(resolved);
119
+ }
120
+ }
121
+ else {
122
+ result = explorer.search(this.getOriginalCwd());
123
+ }
98
124
  // Merge user config with DEFAULT_SYNC_CONFIG (user config takes precedence)
99
125
  // This ensures environment variables are used for database settings
100
126
  // when not explicitly set in the config file
@@ -1 +1 @@
1
- {"version":3,"file":"config-manager.js","sourceRoot":"","sources":["../../src/lib/config-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config-manager.js","sourceRoot":"","sources":["../../src/lib/config-manager.ts"],"names":[],"mappings":"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"}
@@ -7,9 +7,10 @@
7
7
  * special reference types (@file, @url, @lookup, @env, @parent, @root, @template),
8
8
  * manages entity operations, and provides utilities for data transformation.
9
9
  */
10
- import { EntityInfo, BaseEntity, UserInfo } from '@memberjunction/core';
10
+ import { EntityInfo, IMetadataProvider, BaseEntity, UserInfo } from '@memberjunction/core';
11
11
  import { EntityConfig } from '../config.js';
12
12
  import { BatchContextIndex, BatchContextStub } from './batch-context-index.js';
13
+ import { SyncMetadataEngine } from './sync-metadata-engine.js';
13
14
  /** Accepted types for the batchContext parameter (indexed or plain Map). */
14
15
  export type BatchContext = BatchContextIndex | Map<string, BaseEntity | BatchContextStub>;
15
16
  /**
@@ -109,6 +110,19 @@ export interface RecordData {
109
110
  export declare class SyncEngine {
110
111
  private metadata;
111
112
  private contextUser;
113
+ private syncMetadataEngine;
114
+ setMetadataEngine(engine: SyncMetadataEngine): void;
115
+ getMetadataEngine(): SyncMetadataEngine | null;
116
+ /**
117
+ * Returns the metadata provider this engine is using.
118
+ *
119
+ * `mj sync` runs as a single-purpose CLI process with one configured
120
+ * provider per run, so this proxies to the global default (the only
121
+ * provider the process will ever see). Surfacing it through SyncEngine
122
+ * keeps callers from reaching for `Metadata.Provider` directly and makes
123
+ * it obvious which provider the sync is bound to.
124
+ */
125
+ getProvider(): IMetadataProvider;
112
126
  /**
113
127
  * Creates a new SyncEngine instance
114
128
  * @param contextUser - The user context for database operations
@@ -201,6 +215,12 @@ export declare class SyncEngine {
201
215
  * }
202
216
  * ```
203
217
  */
218
+ /**
219
+ * Build a canonical, collision-safe cache key for a multi-field lookup.
220
+ * Encodes each field name + value so embedded `=` / `&` / `|` in user
221
+ * data can't run two distinct lookups into the same key.
222
+ */
223
+ private buildLookupCacheKey;
204
224
  resolveLookup(entityName: string, lookupFields: Array<{
205
225
  fieldName: string;
206
226
  fieldValue: string;
@@ -1 +1 @@
1
- {"version":3,"file":"sync-engine.d.ts","sourceRoot":"","sources":["../../src/lib/sync-engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sync-engine.d.ts","sourceRoot":"","sources":["../../src/lib/sync-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAqB,UAAU,EAAgB,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAG5H,OAAO,EAAE,YAAY,EAAgB,MAAM,WAAW,CAAC;AAEvD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAS5D,4EAA4E;AAC5E,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,GAAG,gBAAgB,CAAC,CAAC;AAE1F;;;;GAIG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,sCAAsC;IACtC,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,+CAA+C;IAC/C,SAAgB,YAAY,EAAE,KAAK,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;IAC7E,uCAAuC;IACvC,SAAgB,aAAa,EAAE,MAAM,CAAC;IACtC,gDAAgD;IAChD,SAAgB,eAAe,CAAC,EAAE,MAAM,CAAC;gBAGvC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,KAAK,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAC,CAAC,EAC5D,aAAa,EAAE,MAAM,EACrB,eAAe,CAAC,EAAE,MAAM;CAY3B;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,2FAA2F;IAC3F,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC1B,mGAAmG;IACnG,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,UAAU,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,4FAA4F;IAC5F,MAAM,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,oCAAoC;IACpC,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,gDAAgD;IAChD,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;IAC/C,mDAAmD;IACnD,IAAI,CAAC,EAAE;QACL,yCAAyC;QACzC,YAAY,EAAE,MAAM,CAAC;QACrB,2CAA2C;QAC3C,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,qEAAqE;IACrE,YAAY,CAAC,EAAE;QACb,qDAAqD;QACrD,MAAM,EAAE,OAAO,CAAC;QAChB,uDAAuD;QACvD,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,yEAAyE;QACzE,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC;CACH;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,WAAW,CAAW;IAC9B,OAAO,CAAC,kBAAkB,CAAmC;IAEtD,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI;IAInD,iBAAiB,IAAI,kBAAkB,GAAG,IAAI;IAIrD;;;;;;;;OAQG;IACI,WAAW,IAAI,iBAAiB;IAIvC;;;OAGG;gBACS,WAAW,EAAE,QAAQ;IAKjC;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAKjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2CG;IACG,iBAAiB,CACrB,KAAK,EAAE,GAAG,EACV,OAAO,EAAE,MAAM,EACf,YAAY,CAAC,EAAE,UAAU,GAAG,gBAAgB,GAAG,IAAI,EACnD,UAAU,CAAC,EAAE,UAAU,GAAG,gBAAgB,GAAG,IAAI,EACjD,KAAK,GAAE,MAAU,EACjB,YAAY,CAAC,EAAE,YAAY,EAC3B,mBAAmB,CAAC,EAAE,uBAAuB,EAC7C,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,GAAG,CAAC;IA6Pf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoCG;IACH;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAarB,aAAa,CACjB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,KAAK,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAC,CAAC,EAC5D,UAAU,GAAE,OAAe,EAC3B,YAAY,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EACtC,YAAY,CAAC,EAAE,YAAY,EAC3B,UAAU,GAAE,OAAe,EAC3B,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC;IA+OlB;;;;;;;;;;;OAWG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IA8B/F;;;;;;OAMG;YACW,gBAAgB;IAe9B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,iBAAiB,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM;IAQpC;;;OAGG;IACH,OAAO,CAAC,cAAc;IAatB;;;;;;;;;;OAUG;IACG,gCAAgC,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKrF;;;;;;;;;;;OAWG;YACW,gCAAgC;IA6E9C;;;;;;;;;;;;;;;;OAgBG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAIpD;;;;;;;;;;;;;;;;;OAiBG;IACG,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAQjE;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACG,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IA0DjG;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;YACW,8BAA8B;IAuD5C;;;;;;;;;;;;;;;OAeG;YACW,sBAAsB;CA0DrC"}
@@ -47,11 +47,30 @@ export class DeferrableLookupError extends Error {
47
47
  * ```
48
48
  */
49
49
  export class SyncEngine {
50
+ setMetadataEngine(engine) {
51
+ this.syncMetadataEngine = engine;
52
+ }
53
+ getMetadataEngine() {
54
+ return this.syncMetadataEngine;
55
+ }
56
+ /**
57
+ * Returns the metadata provider this engine is using.
58
+ *
59
+ * `mj sync` runs as a single-purpose CLI process with one configured
60
+ * provider per run, so this proxies to the global default (the only
61
+ * provider the process will ever see). Surfacing it through SyncEngine
62
+ * keeps callers from reaching for `Metadata.Provider` directly and makes
63
+ * it obvious which provider the sync is bound to.
64
+ */
65
+ getProvider() {
66
+ return Metadata.Provider; // global-provider-ok: CLI single-process; see class doc
67
+ }
50
68
  /**
51
69
  * Creates a new SyncEngine instance
52
70
  * @param contextUser - The user context for database operations
53
71
  */
54
72
  constructor(contextUser) {
73
+ this.syncMetadataEngine = null;
55
74
  this.metadata = new Metadata(); // global-provider-ok: metadata sync operates on the configured provider only
56
75
  this.contextUser = contextUser;
57
76
  }
@@ -350,13 +369,43 @@ export class SyncEngine {
350
369
  * }
351
370
  * ```
352
371
  */
372
+ /**
373
+ * Build a canonical, collision-safe cache key for a multi-field lookup.
374
+ * Encodes each field name + value so embedded `=` / `&` / `|` in user
375
+ * data can't run two distinct lookups into the same key.
376
+ */
377
+ buildLookupCacheKey(entityName, lookupFields) {
378
+ const sortedFields = [...lookupFields].sort((a, b) => a.fieldName.localeCompare(b.fieldName));
379
+ const parts = sortedFields.map(f => {
380
+ const name = encodeURIComponent(f.fieldName.toLowerCase());
381
+ const value = encodeURIComponent((f.fieldValue ?? '').toLowerCase());
382
+ return `${name}=${value}`;
383
+ });
384
+ return `${encodeURIComponent(entityName.toLowerCase())}|${parts.join('&')}`;
385
+ }
353
386
  async resolveLookup(entityName, lookupFields, autoCreate = false, createFields = {}, batchContext, allowDefer = false, originalValue) {
387
+ const lookupCacheKey = this.buildLookupCacheKey(entityName, lookupFields);
388
+ if (this.syncMetadataEngine) {
389
+ const cachedId = this.syncMetadataEngine.getCachedLookup(lookupCacheKey);
390
+ // Use `!== undefined` so legitimately-falsy PK values (empty string, '0')
391
+ // are still served from cache instead of forcing a re-query.
392
+ if (cachedId !== undefined) {
393
+ return cachedId;
394
+ }
395
+ }
396
+ const entityInfo = this.metadata.EntityByName(entityName);
397
+ if (!entityInfo) {
398
+ throw new Error(`Entity not found: ${entityName}`);
399
+ }
354
400
  // First check batch context for in-memory entities
355
401
  if (batchContext) {
356
402
  if (batchContext instanceof BatchContextIndex) {
357
403
  // O(1) indexed lookup
358
404
  const pkValue = batchContext.lookupByFields(entityName, lookupFields);
359
405
  if (pkValue !== undefined) {
406
+ if (this.syncMetadataEngine) {
407
+ this.syncMetadataEngine.setCachedLookup(lookupCacheKey, pkValue, entityName);
408
+ }
360
409
  return pkValue;
361
410
  }
362
411
  }
@@ -378,22 +427,43 @@ export class SyncEngine {
378
427
  }
379
428
  if (allMatch) {
380
429
  // Found in batch context, return primary key
381
- const entityInfo = this.metadata.EntityByName(entityName);
382
- if (entityInfo && entityInfo.PrimaryKeys.length > 0) {
430
+ if (entityInfo.PrimaryKeys.length > 0) {
383
431
  const pkeyField = entityInfo.PrimaryKeys[0].Name;
384
- return entity.Get(pkeyField);
432
+ const id = entity.Get(pkeyField);
433
+ if (this.syncMetadataEngine) {
434
+ this.syncMetadataEngine.setCachedLookup(lookupCacheKey, id, entityName);
435
+ }
436
+ return id;
385
437
  }
386
438
  }
387
439
  }
388
440
  }
389
441
  }
390
442
  }
443
+ // Scan preloaded entities in-memory if preloaded
444
+ if (this.syncMetadataEngine && this.syncMetadataEngine.isEntityPreloaded(entityName)) {
445
+ const cachedEntities = this.syncMetadataEngine.getCachedEntities(entityName);
446
+ for (const cachedEntity of cachedEntities) {
447
+ let allMatch = true;
448
+ for (const { fieldName, fieldValue } of lookupFields) {
449
+ const entityValue = cachedEntity.Get(fieldName);
450
+ const normalizedEntityValue = (entityValue?.toString() || '').toLowerCase().trim();
451
+ const normalizedLookupValue = (fieldValue?.toString() || '').toLowerCase().trim();
452
+ if (normalizedEntityValue !== normalizedLookupValue) {
453
+ allMatch = false;
454
+ break;
455
+ }
456
+ }
457
+ if (allMatch) {
458
+ const pkeyField = entityInfo.PrimaryKeys[0].Name;
459
+ const id = cachedEntity.Get(pkeyField);
460
+ this.syncMetadataEngine.setCachedLookup(lookupCacheKey, id, entityName);
461
+ return id;
462
+ }
463
+ }
464
+ }
391
465
  // Not found in batch context, check database
392
466
  const rv = new RunView();
393
- const entityInfo = this.metadata.EntityByName(entityName);
394
- if (!entityInfo) {
395
- throw new Error(`Entity not found: ${entityName}`);
396
- }
397
467
  // Build compound filter for all lookup fields
398
468
  const filterParts = [];
399
469
  for (const { fieldName, fieldValue } of lookupFields) {
@@ -463,6 +533,9 @@ export class SyncEngine {
463
533
  if (entityInfo.PrimaryKeys.length > 0) {
464
534
  const pkeyField = entityInfo.PrimaryKeys[0].Name;
465
535
  const id = result.Results[0][pkeyField];
536
+ if (this.syncMetadataEngine) {
537
+ this.syncMetadataEngine.setCachedLookup(lookupCacheKey, id, entityName);
538
+ }
466
539
  return id;
467
540
  }
468
541
  }
@@ -508,6 +581,10 @@ export class SyncEngine {
508
581
  if (entityInfo.PrimaryKeys.length > 0) {
509
582
  const pkeyField = entityInfo.PrimaryKeys[0].Name;
510
583
  const newId = newEntity.Get(pkeyField);
584
+ if (this.syncMetadataEngine) {
585
+ this.syncMetadataEngine.setCachedLookup(lookupCacheKey, newId, entityName);
586
+ this.syncMetadataEngine.addEntityToCache(entityName, newEntity);
587
+ }
511
588
  return newId;
512
589
  }
513
590
  }
@@ -791,6 +868,13 @@ export class SyncEngine {
791
868
  if (!entityInfo) {
792
869
  throw new Error(`Entity not found: ${entityName}`);
793
870
  }
871
+ if (this.syncMetadataEngine && this.syncMetadataEngine.isEntityPreloaded(entityName)) {
872
+ // O(1) PK lookup against the preload cache. The previous Array.find
873
+ // + per-entity serializePrimaryKey(GetAll()) was O(N×K) overall and
874
+ // dominated runtime for entities with large DB-side populations
875
+ // (Integration Object Fields: 38min → seconds).
876
+ return this.syncMetadataEngine.findCachedByPrimaryKey(entityName, primaryKey);
877
+ }
794
878
  // First, check if the record exists using RunView to avoid "Error in BaseEntity.Load" messages
795
879
  // when records don't exist (which is a normal scenario during sync operations)
796
880
  const rv = new RunView();
@@ -821,7 +905,11 @@ export class SyncEngine {
821
905
  const compositeKey = new CompositeKey();
822
906
  compositeKey.LoadFromSimpleObject(primaryKey);
823
907
  const loaded = await entity.InnerLoad(compositeKey);
824
- return loaded ? entity : null;
908
+ const loadedEntity = loaded ? entity : null;
909
+ if (loadedEntity && this.syncMetadataEngine) {
910
+ this.syncMetadataEngine.addEntityToCache(entityName, loadedEntity);
911
+ }
912
+ return loadedEntity;
825
913
  }
826
914
  /**
827
915
  * Process file content with {@include} references