ngx-dynamic-entity 2.5.0 → 3.0.0-rc.1

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.
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
2
2
  import { InjectionToken, makeEnvironmentProviders, inject, Injectable, signal, Input, ChangeDetectionStrategy, Component, ChangeDetectorRef, DestroyRef, Injector, isDevMode, ViewContainerRef, ViewChild, forwardRef, Optional, Inject, EventEmitter, ElementRef, computed, afterNextRender, HostListener, ViewChildren, Output } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { ReactiveFormsModule, Validators, FormBuilder, FormGroup, FormArray, FormsModule } from '@angular/forms';
5
- import { resolveLabel, formatDisplayValue, normalizeLookupValues, lookupValuesToOptions, valuesMatch, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, isValidUrl, isValidPhone, fileRefName, sliderBounds, ratingScale, normalizeHexColor, normalizeTags, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, fieldsUnderTab, getTabPath, fieldRefFor, flattenFieldValues, parseFieldRef, placeTabFields, normalizeArrayStructures, getTabData, getValueByPath, normalizeConfigOptions, migrateRecord, ambiguousFieldIds, toRefToken, collectFieldScopes, applyAutoPatch, refOf, isUnsafePath, valuesEqual, parseCsv, arrayBoundFor, deriveImportColumns, suggestMapping, cellText, applyMapping, decimalMarkFor, toCsv, stripIndices, CORE_VERSION, DEFAULT_ARRAY_ROWS, upgradeLegacyRefs, coerceCell, buildTemplateSpec, collectLeafTargets, formatConfigProblems } from '@dynamic-entity/core';
5
+ import { resolveLabel, formatDisplayValue, normalizeLookupValues, lookupValuesToOptions, valuesMatch, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, isValidUrl, isValidPhone, fileRefName, sliderBounds, ratingScale, normalizeHexColor, normalizeTags, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, fieldsUnderTab, getTabPath, fieldRefFor, flattenFieldValues, parseFieldRef, placeTabFields, normalizeArrayStructures, getTabData, getValueByPath, normalizeConfigOptions, migrateRecord, ambiguousFieldIds, toRefToken, collectFieldScopes, applyAutoPatch, refOf, isUnsafePath, valuesEqual, parseCsv, arrayBoundFor, deriveImportColumns, suggestMapping, cellText, applyMapping, decimalMarkFor, toCsv, stripIndices, CORE_VERSION, DEFAULT_ARRAY_ROWS, planForServer, upgradeLegacyRefs, coerceCell, buildTemplateSpec, collectLeafTargets, formatConfigProblems } from '@dynamic-entity/core';
6
6
  export { formatDisplayValue, normalizeConfig, normalizeOption, resolveLabel, resolveOptionLabel, resolveOptionValue, valuesMatch } from '@dynamic-entity/core';
7
7
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
8
  import { Subject, isObservable, firstValueFrom } from 'rxjs';
@@ -10178,6 +10178,10 @@ class LocalImportTransport {
10178
10178
  pending.catch(() => this.parsed.delete(file));
10179
10179
  return pending;
10180
10180
  }
10181
+ /**
10182
+ * The suggestion made here is always plan v2, and the return type says so. The contract's is
10183
+ * a plan in either shape, because a 2.x server sends the older one.
10184
+ */
10181
10185
  async preview(file, context) {
10182
10186
  const sheet = await this.read(file);
10183
10187
  // Sized from the headers exactly as the server sizes them, so both offer the same slots.
@@ -10314,6 +10318,8 @@ function selectionOf(spec, context, fields) {
10314
10318
  * to resolve — and a consumer who wants two of them, pointed at two backends, gets that for
10315
10319
  * free.
10316
10320
  */
10321
+ /** What a Dynamic Entity server calls its adapter in the `adapter` stamp on every answer. */
10322
+ const ADAPTER_NAME = 'dynamic-entity';
10317
10323
  /** The first release whose server says which date order it reads with, and refuses to guess one. */
10318
10324
  const DATE_ORDER_SINCE = [2, 5];
10319
10325
  /** Whether an engine version is from before the server stopped reading dates month first. */
@@ -10330,6 +10336,12 @@ class HttpImportTransport {
10330
10336
  * Keyed on the `File`, so it lives exactly as long as the file does.
10331
10337
  */
10332
10338
  reported = new WeakMap();
10339
+ /**
10340
+ * The plan versions each previewed file's server said it reads. A file with no entry was
10341
+ * never previewed here, and one whose server said nothing is from before plan v2: either
10342
+ * way `commit` sends nothing that only a v2 reader would understand.
10343
+ */
10344
+ planVersions = new WeakMap();
10333
10345
  constructor(options) {
10334
10346
  this.options = options;
10335
10347
  }
@@ -10382,6 +10394,22 @@ class HttpImportTransport {
10382
10394
  `was built against ${CORE_VERSION}. The two halves of an import are supposed to be the ` +
10383
10395
  'same engine — bring them into step before trusting a difference in what they produce.');
10384
10396
  }
10397
+ /**
10398
+ * Warn when the server runs another adapter, or this one at another major version.
10399
+ *
10400
+ * Since 3.0 a server says which adapter turned its rows into records. A server from before
10401
+ * that says nothing, and `checkEngine` has already compared the one version it does report.
10402
+ */
10403
+ checkAdapter(adapter) {
10404
+ if (!adapter)
10405
+ return;
10406
+ const major = (version) => String(version ?? '').split('.')[0];
10407
+ if (adapter.name === ADAPTER_NAME && major(adapter.version) === major(CORE_VERSION))
10408
+ return;
10409
+ this.warn(`[ngx-dynamic-entity] The import server reads rows with the adapter ${adapter.name} ${adapter.version}; ` +
10410
+ `this app expects ${ADAPTER_NAME} ${major(CORE_VERSION)}.x. What the server imports may differ from ` +
10411
+ 'what the review step showed. Bring the two into step.');
10412
+ }
10385
10413
  async preview(file, context) {
10386
10414
  const body = new FormData();
10387
10415
  body.append('file', file, file.name || 'upload');
@@ -10394,6 +10422,8 @@ class HttpImportTransport {
10394
10422
  throw await failureOf(response);
10395
10423
  const preview = (await response.json());
10396
10424
  this.checkEngine(preview.engineVersion);
10425
+ this.checkAdapter(preview.adapter);
10426
+ this.planVersions.set(file, Array.isArray(preview.planVersions) ? preview.planVersions : undefined);
10397
10427
  this.reported.set(file, preview.dateOrder);
10398
10428
  this.checkDateOrder(context?.dateOrder, preview.dateOrder, preview.engineVersion);
10399
10429
  return {
@@ -10415,11 +10445,21 @@ class HttpImportTransport {
10415
10445
  throw new Error(`This app reads dates as ${context.dateOrder} but the import server reads them as ${reported}. ` +
10416
10446
  'Nothing was imported. Set the same dateOrder on the wizard and on the server\'s router.');
10417
10447
  }
10448
+ // Also before a byte is sent. A server from before plan v2 ignores what it does not know,
10449
+ // so a plan that groups rows would import one record per row there and report success.
10450
+ const sendable = planForServer(plan, this.planVersions.get(file));
10451
+ if (sendable.refused) {
10452
+ throw new Error(`This mapping uses ${sendable.refused.join(', ')}, which the import server does not read: it is from ` +
10453
+ 'before mapping plan v2 and would ignore it. Nothing was imported. Upgrade the server to ' +
10454
+ '@dynamic-entity/server 3.0 or later.');
10455
+ }
10418
10456
  const body = new FormData();
10419
10457
  // **The plan first, and the file second.** The server starts importing while the file is
10420
10458
  // still arriving — that is the whole reason it exists — so the plan has to have arrived
10421
10459
  // already. `FormData` preserves append order, so this ordering is the contract.
10422
- body.append('plan', JSON.stringify(plan));
10460
+ // As written for a server that reads plan v2, and in 2.x form (`entity`, `configVersion`)
10461
+ // for one that does not: the same mapping, in the words that server knows.
10462
+ body.append('plan', JSON.stringify(sendable.plan));
10423
10463
  body.append('file', file, file.name || 'upload');
10424
10464
  const response = await this.send(`${this.base}/${this.entityOf(context)}/import`, {
10425
10465
  method: 'POST',
@@ -10430,6 +10470,7 @@ class HttpImportTransport {
10430
10470
  throw await failureOf(response);
10431
10471
  const result = (await response.json());
10432
10472
  this.checkEngine(result.engineVersion);
10473
+ this.checkAdapter(result.adapter);
10433
10474
  return {
10434
10475
  // Empty, and not because nothing was imported. The server streamed the file precisely so
10435
10476
  // that fifty thousand records never had to exist at once; sending them back would undo
@@ -10443,6 +10484,8 @@ class HttpImportTransport {
10443
10484
  errorCount: result.errorCount ?? result.errors?.length ?? 0,
10444
10485
  truncated: result.truncated ?? false,
10445
10486
  planProblems: result.planProblems ?? [],
10487
+ // Only a 3.0 server sends them, and only a run with something to warn about has any.
10488
+ ...(result.warnings?.length ? { warnings: result.warnings } : {}),
10446
10489
  };
10447
10490
  }
10448
10491
  async template(spec, format, context, fields) {
@@ -10587,10 +10630,10 @@ const CONSTANT = -1;
10587
10630
  class ImportMapperComponent {
10588
10631
  config;
10589
10632
  headers = [];
10590
- /** The starting point, normally `suggestMapping`'s output. */
10633
+ /** The starting point, normally `suggestMapping`'s output. A stored 2.x plan is taken too. */
10591
10634
  plan = null;
10592
10635
  language = 'en';
10593
- /** Emitted whenever the user changes a selection. */
10636
+ /** Emitted whenever the user changes a selection. Always plan v2, whichever shape came in. */
10594
10637
  planChange = new EventEmitter();
10595
10638
  ui = inject(UiTextService);
10596
10639
  rows = signal([], ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
@@ -10703,6 +10746,10 @@ class ImportMapperComponent {
10703
10746
  * Every entry of the incoming plan survives in its own position — a fixed value, a ref the
10704
10747
  * config no longer has — unless the user changed or removed it, and an untouched entry is
10705
10748
  * emitted exactly as it arrived. New selections follow, in field order.
10749
+ *
10750
+ * What is emitted is plan v2, whichever shape came in: `target` and `schemaVersion`, where a
10751
+ * 2.x plan said `entity` and `configVersion`. Anything else the incoming plan carried, such
10752
+ * as its lists and its grouping, is kept.
10706
10753
  */
10707
10754
  toPlan() {
10708
10755
  const rowByRef = new Map(this.rows().map(row => [row.column.ref, row]));
@@ -10740,10 +10787,13 @@ class ImportMapperComponent {
10740
10787
  if (entry)
10741
10788
  entries.push(entry);
10742
10789
  }
10790
+ // The 2.x names go: a plan carrying both is one the reader has to warn about.
10791
+ const kept = Object.fromEntries(Object.entries(this.incoming ?? {}).filter(([key]) => key !== 'entity' && key !== 'configVersion'));
10743
10792
  return {
10744
- ...this.incoming,
10745
- entity: this.config?.entity ?? '',
10746
- configVersion: this.config?.version,
10793
+ ...kept,
10794
+ planVersion: 2,
10795
+ target: this.config?.entity ?? '',
10796
+ schemaVersion: this.config?.version,
10747
10797
  sourceHeaders: [...this.headers],
10748
10798
  entries,
10749
10799
  };
@@ -11042,7 +11092,8 @@ class ImportPreviewComponent {
11042
11092
  lang: this.language,
11043
11093
  lookups: this.lookups,
11044
11094
  decimal: this.decimal,
11045
- dateOrder: this.dateOrder,
11095
+ // The column's own order when its plan entry states one, as the commit reads it.
11096
+ dateOrder: entry.dateOrder ?? this.dateOrder,
11046
11097
  });
11047
11098
  if ('error' in outcome)
11048
11099
  return { text: outcome.error, failed: true };