@memberjunction/metadata-sync 5.37.0 → 5.39.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 +145 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/config-manager.d.ts +13 -5
- package/dist/lib/config-manager.d.ts.map +1 -1
- package/dist/lib/config-manager.js +34 -8
- package/dist/lib/config-manager.js.map +1 -1
- package/dist/lib/sync-engine.d.ts +21 -1
- package/dist/lib/sync-engine.d.ts.map +1 -1
- package/dist/lib/sync-engine.js +96 -8
- package/dist/lib/sync-engine.js.map +1 -1
- package/dist/lib/sync-metadata-engine.d.ts +176 -0
- package/dist/lib/sync-metadata-engine.d.ts.map +1 -0
- package/dist/lib/sync-metadata-engine.js +454 -0
- package/dist/lib/sync-metadata-engine.js.map +1 -0
- package/dist/services/PushService.d.ts +1 -0
- package/dist/services/PushService.d.ts.map +1 -1
- package/dist/services/PushService.js +44 -11
- package/dist/services/PushService.js.map +1 -1
- package/dist/services/ValidationService.d.ts.map +1 -1
- package/dist/services/ValidationService.js +13 -5
- package/dist/services/ValidationService.js.map +1 -1
- package/package.json +12 -12
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](../
|
|
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/
|
|
1853
|
-
- [@memberjunction/core-entities](../MJCoreEntities/
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
*
|
|
43
|
-
*
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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;
|
|
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
|
-
*
|
|
84
|
-
*
|
|
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
|
-
*
|
|
87
|
-
*
|
|
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
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
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":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"config-manager.js","sourceRoot":"","sources":["../../src/lib/config-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,2CAA2C,CAAC;AAErF,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B,MAAM,eAAe,GAAG,wBAAwB,EAAE,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,MAAM,mBAAmB,GAAsB;IAC7C,kEAAkE;IAClE,UAAU,EAAE,eAAe,IAAI,WAAW;IAC1C,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,WAAW;IAC1C,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO;QACzB,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC;QACnC,CAAC,CAAC,CAAC,eAAe,KAAK,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACpD,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW;IACnC,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW;IACnC,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW;IACnC,wBAAwB,EAAE,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;IAC9F,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,SAAS;IAC9C,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB;IAC5C,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,MAAM;CACnD,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,OAAO,aAAc,SAAQ,aAA4B;IAK7D;QACE,KAAK,EAAE,CAAC;QALF,gBAAW,GAAkB,IAAI,CAAC;QAClC,aAAQ,GAAoB,IAAI,CAAC;QACjC,iBAAY,GAAG,KAAK,CAAC;IAI7B,CAAC;IAED;;OAEG;IACI,MAAM,KAAK,QAAQ;QACxB,OAAO,aAAa,CAAC,WAAW,EAAiB,CAAC;IACpD,CAAC;IAED;;;;OAIG;IACH,cAAc;QACZ,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,0BAA0B;YAC1B,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;QACnC,CAAC;QACD,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED;;;;OAIG;IACH,cAAc,CAAC,GAAW;QACxB,IAAI,CAAC,WAAW,GAAG,GAAG,CAAC;IACzB,CAAC;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,YAAY,CAAC,WAAW,GAAG,KAAK;QAC9B,IAAI,IAAI,CAAC,YAAY,IAAI,CAAC,WAAW,EAAE,CAAC;YACtC,OAAO,IAAI,CAAC,QAAQ,CAAC;QACvB,CAAC;QAED,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,MAA0C,CAAC;YAE/C,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;YAC/C,IAAI,WAAW,IAAI,WAAW,CAAC,IAAI,EAAE,EAAE,CAAC;gBACtC,qEAAqE;gBACrE,sEAAsE;gBACtE,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;oBAC3C,CAAC,CAAC,WAAW;oBACb,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,WAAW,CAAC,CAAC;gBACrD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;oBAC7B,OAAO,CAAC,KAAK,CAAC,6BAA6B,QAAQ,4DAA4D,CAAC,CAAC;oBACjH,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC;gBAClD,CAAC;qBAAM,CAAC;oBACN,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACnC,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC;YAClD,CAAC;YAED,4EAA4E;YAC5E,oEAAoE;YACpE,6CAA6C;YAC7C,MAAM,UAAU,GAAG,MAAM,EAAE,MAAM,IAAI,EAAE,CAAC;YACxC,IAAI,CAAC,QAAQ,GAAG,YAAY,CAAC,mBAAmB,EAAE,UAAU,CAAa,CAAC;YAC1E,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;YACzB,OAAO,IAAI,CAAC,QAAQ,CAAC;QACvB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,0BAA0B,EAAE,KAAK,CAAC,CAAC;YACjD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;YACzB,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,WAAW;QACT,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC,YAAY,EAAE,CAAC;QAC7B,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;CACF;AAED,4CAA4C;AAC5C,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAC,QAAQ,CAAC"}
|
|
@@ -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":"AAAA;;;;;;;;GAQG;AAMH,OAAO,EAAE,UAAU,EAAqB,UAAU,EAAgB,QAAQ,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"sync-engine.d.ts","sourceRoot":"","sources":["../../src/lib/sync-engine.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/sync-engine.js
CHANGED
|
@@ -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
|
-
|
|
382
|
-
if (entityInfo && entityInfo.PrimaryKeys.length > 0) {
|
|
430
|
+
if (entityInfo.PrimaryKeys.length > 0) {
|
|
383
431
|
const pkeyField = entityInfo.PrimaryKeys[0].Name;
|
|
384
|
-
|
|
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
|
-
|
|
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
|