ngx-dynamic-entity 0.0.1 → 1.1.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 +129 -0
- package/esm2022/lib/field-types/array-field.component.mjs +160 -0
- package/esm2022/lib/field-types/boolean-field.component.mjs +100 -0
- package/esm2022/lib/field-types/checkbox-field.component.mjs +25 -17
- package/esm2022/lib/field-types/currency-field.component.mjs +101 -0
- package/esm2022/lib/field-types/date-field.component.mjs +21 -13
- package/esm2022/lib/field-types/dropdown-field.component.mjs +120 -29
- package/esm2022/lib/field-types/email-field.component.mjs +101 -0
- package/esm2022/lib/field-types/entity-ref-field.component.mjs +96 -32
- package/esm2022/lib/field-types/file-field.component.mjs +156 -0
- package/esm2022/lib/field-types/group-field.component.mjs +89 -0
- package/esm2022/lib/field-types/image-field.component.mjs +168 -0
- package/esm2022/lib/field-types/month-year-field.component.mjs +151 -0
- package/esm2022/lib/field-types/multi-select-field.component.mjs +79 -25
- package/esm2022/lib/field-types/number-field.component.mjs +60 -17
- package/esm2022/lib/field-types/password-field.component.mjs +109 -0
- package/esm2022/lib/field-types/radio-field.component.mjs +152 -0
- package/esm2022/lib/field-types/text-field.component.mjs +58 -18
- package/esm2022/lib/field-types/textarea-field.component.mjs +26 -15
- package/esm2022/lib/form/dynamic-field/dynamic-field.component.mjs +44 -26
- package/esm2022/lib/form/dynamic-form.component.mjs +676 -69
- package/esm2022/lib/form/dynamic-record-form.component.mjs +494 -0
- package/esm2022/lib/providers/provide-field-types.mjs +72 -0
- package/esm2022/lib/providers/provide-ngx-dynamic-entity.mjs +4 -5
- package/esm2022/lib/services/cascade-data.service.mjs +129 -0
- package/esm2022/lib/services/config-source.service.mjs +67 -0
- package/esm2022/lib/services/entity-ref-cache.mjs +20 -0
- package/esm2022/lib/services/entity-ref-registry.service.mjs +3 -3
- package/esm2022/lib/services/entity-ref-selection.service.mjs +27 -0
- package/esm2022/lib/services/entity-reference.service.mjs +168 -0
- package/esm2022/lib/services/field-registry.service.mjs +35 -30
- package/esm2022/lib/services/file-upload.service.mjs +89 -0
- package/esm2022/lib/services/lookup-registry.service.mjs +246 -0
- package/esm2022/lib/services/rbac.service.mjs +5 -11
- package/esm2022/lib/services/rules-evaluation.service.mjs +27 -0
- package/esm2022/lib/services/validator-registry.service.mjs +35 -7
- package/esm2022/lib/tokens/injection-tokens.mjs +104 -7
- package/esm2022/public-api.mjs +31 -6
- package/fesm2022/ngx-dynamic-entity.mjs +4094 -759
- package/fesm2022/ngx-dynamic-entity.mjs.map +1 -1
- package/lib/field-types/array-field.component.d.ts +22 -0
- package/lib/field-types/boolean-field.component.d.ts +18 -0
- package/lib/field-types/checkbox-field.component.d.ts +3 -2
- package/lib/field-types/currency-field.component.d.ts +17 -0
- package/lib/field-types/date-field.component.d.ts +3 -2
- package/lib/field-types/dropdown-field.component.d.ts +24 -4
- package/lib/field-types/email-field.component.d.ts +15 -0
- package/lib/field-types/entity-ref-field.component.d.ts +24 -11
- package/lib/field-types/file-field.component.d.ts +25 -0
- package/lib/field-types/group-field.component.d.ts +17 -0
- package/lib/field-types/image-field.component.d.ts +30 -0
- package/lib/field-types/month-year-field.component.d.ts +28 -0
- package/lib/field-types/multi-select-field.component.d.ts +18 -4
- package/lib/field-types/number-field.component.d.ts +5 -2
- package/lib/field-types/password-field.component.d.ts +17 -0
- package/lib/field-types/radio-field.component.d.ts +33 -0
- package/lib/field-types/text-field.component.d.ts +5 -3
- package/lib/field-types/textarea-field.component.d.ts +4 -2
- package/lib/form/dynamic-field/dynamic-field.component.d.ts +20 -7
- package/lib/form/dynamic-form.component.d.ts +203 -24
- package/lib/form/dynamic-record-form.component.d.ts +180 -0
- package/lib/providers/provide-field-types.d.ts +24 -0
- package/lib/providers/provide-ngx-dynamic-entity.d.ts +19 -8
- package/lib/services/cascade-data.service.d.ts +45 -0
- package/lib/services/config-source.service.d.ts +14 -0
- package/lib/services/entity-ref-cache.d.ts +27 -0
- package/lib/services/entity-ref-registry.service.d.ts +4 -3
- package/lib/services/entity-ref-selection.service.d.ts +24 -0
- package/lib/services/entity-reference.service.d.ts +49 -0
- package/lib/services/field-registry.service.d.ts +17 -7
- package/lib/services/file-upload.service.d.ts +27 -0
- package/lib/services/lookup-registry.service.d.ts +94 -0
- package/lib/services/rbac.service.d.ts +4 -9
- package/lib/services/rules-evaluation.service.d.ts +17 -0
- package/lib/services/validator-registry.service.d.ts +5 -4
- package/lib/tokens/injection-tokens.d.ts +113 -8
- package/package.json +18 -8
- package/public-api.d.ts +21 -4
- package/esm2022/lib/services/config.service.mjs +0 -58
- package/esm2022/lib/services/version.service.mjs +0 -40
- package/esm2022/lib/table/dynamic-table.component.mjs +0 -160
- package/lib/services/config.service.d.ts +0 -25
- package/lib/services/version.service.d.ts +0 -26
- package/lib/table/dynamic-table.component.d.ts +0 -56
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReferenceLoaderContext, ReferenceOption } from '@dynamic-entity/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
/**
|
|
4
|
+
* EntityReferenceService — the cache in front of consumer-registered option loaders.
|
|
5
|
+
*
|
|
6
|
+
* Three layers, matching the reference implementation:
|
|
7
|
+
* 1. **options** — resolved `ReferenceOption[]` per (entity, lang, displayFields, filters).
|
|
8
|
+
* 2. **in-flight** — the pending Promise, so N fields referencing one entity trigger one
|
|
9
|
+
* load rather than N. This is the layer that actually removes duplicate HTTP calls;
|
|
10
|
+
* an options cache alone does nothing for concurrent first loads.
|
|
11
|
+
* 3. **labels** — synchronous value → text, for rendering a stored reference without
|
|
12
|
+
* waiting on a load.
|
|
13
|
+
*
|
|
14
|
+
* Invalidation is per entity, not global. The reference could only `clearCache()` everything
|
|
15
|
+
* (issue #16), so saving one record re-fetched every entity's options.
|
|
16
|
+
*/
|
|
17
|
+
export declare class EntityReferenceService {
|
|
18
|
+
private readonly registry;
|
|
19
|
+
private readonly store;
|
|
20
|
+
/** Loads in progress, keyed the same way as the options cache. */
|
|
21
|
+
private readonly inFlight;
|
|
22
|
+
/** `entityKey|lang|canonicalValue` -> display text. */
|
|
23
|
+
private readonly labels;
|
|
24
|
+
/**
|
|
25
|
+
* Options for an entity, from cache when present.
|
|
26
|
+
* Concurrent callers with the same key share one load.
|
|
27
|
+
*/
|
|
28
|
+
load(entityKey: string, ctx?: ReferenceLoaderContext): Promise<ReferenceOption[]>;
|
|
29
|
+
/** Cached options without triggering a load. `undefined` when not cached. */
|
|
30
|
+
peek(entityKey: string, ctx?: ReferenceLoaderContext): ReferenceOption[] | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Display text for a stored reference value, synchronously.
|
|
33
|
+
* Returns `undefined` when the entity's options have never been loaded.
|
|
34
|
+
*/
|
|
35
|
+
labelFor(entityKey: string, value: unknown, lang?: string): string | undefined;
|
|
36
|
+
/** Drop everything cached for one entity, leaving other entities intact. */
|
|
37
|
+
invalidate(entityKey: string): void;
|
|
38
|
+
clear(): void;
|
|
39
|
+
private rememberLabels;
|
|
40
|
+
/** Label-cache key. An object value collapses to its resolved text for the active language. */
|
|
41
|
+
private labelKey;
|
|
42
|
+
private invoke;
|
|
43
|
+
private toPromise;
|
|
44
|
+
private isSubscribable;
|
|
45
|
+
/** Exposed for cascade filtering, which matches a parent value against option values. */
|
|
46
|
+
matches(a: unknown, b: unknown, lang?: string): boolean;
|
|
47
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<EntityReferenceService, never>;
|
|
48
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<EntityReferenceService>;
|
|
49
|
+
}
|
|
@@ -2,21 +2,31 @@ import { Type } from '@angular/core';
|
|
|
2
2
|
import * as i0 from "@angular/core";
|
|
3
3
|
/**
|
|
4
4
|
* FieldRegistryService — resolves field type strings to Angular component classes.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* **Tree-shaking contract:** this service holds *no* static imports of field components.
|
|
7
|
+
* Nothing is bundled until something registers it, which is what makes the built-ins
|
|
8
|
+
* opt-in. Register them with `provideBuiltInFieldTypes()` (all 19 keys) or a narrower
|
|
9
|
+
* subset via `provideFieldTypes({ ... })` — see `providers/provide-field-types.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Resolution order: consumer token registry → runtime registrations → nothing.
|
|
7
12
|
*/
|
|
8
13
|
export declare class FieldRegistryService {
|
|
9
14
|
private readonly consumerRegistry;
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
private readonly dynamicRegistry;
|
|
16
|
+
constructor();
|
|
17
|
+
/** Register a field type component mapping at runtime. */
|
|
18
|
+
register(type: string, component: Type<any>): void;
|
|
19
|
+
/** Register multiple field type component mappings at runtime. */
|
|
20
|
+
registerAll(map: Record<string, Type<any>>): void;
|
|
12
21
|
/**
|
|
13
22
|
* Resolve a field type string to a component class.
|
|
14
|
-
* Consumer registry takes
|
|
15
|
-
* Returns null if type is unknown — caller must handle gracefully.
|
|
23
|
+
* Consumer token registry takes priority over runtime registrations.
|
|
16
24
|
*/
|
|
17
25
|
resolve(fieldType: string): Type<any> | null;
|
|
18
|
-
/** Check if a field type has a registered component */
|
|
26
|
+
/** Check if a field type has a registered component. */
|
|
19
27
|
has(fieldType: string): boolean;
|
|
28
|
+
/** Every registered field type key. Consumer overrides and runtime registrations combined. */
|
|
29
|
+
registeredTypes(): string[];
|
|
20
30
|
static ɵfac: i0.ɵɵFactoryDeclaration<FieldRegistryService, never>;
|
|
21
31
|
static ɵprov: i0.ɵɵInjectableDeclaration<FieldRegistryService>;
|
|
22
32
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FileRef } from '@dynamic-entity/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
/**
|
|
4
|
+
* FileUploadService — the single place image-field and file-field agree on how a
|
|
5
|
+
* selected `File` becomes a `FileRef`.
|
|
6
|
+
*
|
|
7
|
+
* With `UPLOAD_HANDLER` registered the file is persisted and stored as `{ url }`;
|
|
8
|
+
* without one it is stored as `{ file }` for the consumer to upload at submit time.
|
|
9
|
+
*/
|
|
10
|
+
export declare class FileUploadService {
|
|
11
|
+
private readonly handler;
|
|
12
|
+
get hasHandler(): boolean;
|
|
13
|
+
/** Metadata common to both persisted and unpersisted refs. */
|
|
14
|
+
private describe;
|
|
15
|
+
/**
|
|
16
|
+
* Turn a selected File into the value to store on the control.
|
|
17
|
+
* @throws when a registered handler rejects — callers surface it as a field error.
|
|
18
|
+
*/
|
|
19
|
+
toFileRef(file: File): Promise<FileRef>;
|
|
20
|
+
/** Object URL for previewing an unpersisted File. Callers must revoke it. */
|
|
21
|
+
previewUrlFor(ref: FileRef | null | undefined): string | null;
|
|
22
|
+
revokePreviewUrl(url: string | null): void;
|
|
23
|
+
private normalize;
|
|
24
|
+
private isSubscribable;
|
|
25
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<FileUploadService, never>;
|
|
26
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<FileUploadService>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type WritableSignal } from '@angular/core';
|
|
2
|
+
import type { DropdownOption, LookupListValue, NestedFieldConfig } from '@dynamic-entity/core';
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
/**
|
|
5
|
+
* LookupRegistryService — the cache in front of consumer-registered master lists.
|
|
6
|
+
*
|
|
7
|
+
* Three layers, deliberately the same shape as `EntityReferenceService`, because the problem is
|
|
8
|
+
* the same: one list is typically used by many fields across many entities.
|
|
9
|
+
* 1. **options** — resolved `DropdownOption[]` per (list name, lang).
|
|
10
|
+
* 2. **in-flight** — the pending Promise, so N fields on one list trigger one load, not N.
|
|
11
|
+
* 3. **labels** — synchronous value → text (parity plan §6.2). This is not a nicety: since
|
|
12
|
+
* `viewMode` defaults to true a record opens read-only, so rendering a stored value as
|
|
13
|
+
* text is the *first* thing a `listName` field has to do. An async-only registry would
|
|
14
|
+
* paint the raw stored value and then flip, on every record open.
|
|
15
|
+
*
|
|
16
|
+
* A failed load rejects and caches nothing, so one transient error does not serve an empty
|
|
17
|
+
* list forever — the same fix made for entity references in phase 5.
|
|
18
|
+
*/
|
|
19
|
+
export declare class LookupRegistryService {
|
|
20
|
+
private readonly registry;
|
|
21
|
+
/** `listName|lang` -> resolved options. */
|
|
22
|
+
private readonly options;
|
|
23
|
+
/** `listName|lang` -> normalised values, so a consumer can reach `code` / `isSystemDefined`. */
|
|
24
|
+
private readonly values;
|
|
25
|
+
/** Loads in progress, keyed as above. */
|
|
26
|
+
private readonly inFlight;
|
|
27
|
+
/** `listName|lang|text` -> display text, for the synchronous read-only path. */
|
|
28
|
+
private readonly labels;
|
|
29
|
+
has(listName: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Options for a named list, from cache when present.
|
|
32
|
+
* Concurrent callers with the same key share one load.
|
|
33
|
+
*/
|
|
34
|
+
load(listName: string, lang?: string): Promise<DropdownOption[]>;
|
|
35
|
+
/** Cached options without triggering a load. `undefined` when not loaded. */
|
|
36
|
+
peek(listName: string, lang?: string): DropdownOption[] | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Cached list values, carrying `_id`, `code`, `isSystemDefined` and `from`.
|
|
39
|
+
* The library never reads those; this is the seam for a consumer that needs them.
|
|
40
|
+
*/
|
|
41
|
+
valuesFor(listName: string, lang?: string): LookupListValue[] | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Display text for a stored value, synchronously. `undefined` when the list has not loaded
|
|
44
|
+
* or holds no matching value — the caller keeps its own fallback, which is the stored text.
|
|
45
|
+
*
|
|
46
|
+
* Every language of an option is indexed, so a record saved under one language renders in
|
|
47
|
+
* another: a value stored as `'Aktiv'` resolves to `'Active'` under `lang: 'en'`.
|
|
48
|
+
*/
|
|
49
|
+
labelFor(listName: string | undefined, value: unknown, lang?: string): string | undefined;
|
|
50
|
+
/** True when this field's options have to come from the registry. */
|
|
51
|
+
needsResolve(field: NestedFieldConfig | undefined): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The options to render right now, without awaiting: inline options, else whatever the
|
|
54
|
+
* registry already holds. Empty on a cold miss — pair with `resolveOptions`.
|
|
55
|
+
*/
|
|
56
|
+
optionsFor(field: NestedFieldConfig | undefined, lang?: string): DropdownOption[];
|
|
57
|
+
/** The options for a field, loading the named list if needed. */
|
|
58
|
+
resolveOptions(field: NestedFieldConfig | undefined, lang?: string): Promise<DropdownOption[]>;
|
|
59
|
+
/** Drop everything cached for one list, leaving other lists intact. */
|
|
60
|
+
invalidate(listName: string): void;
|
|
61
|
+
clear(): void;
|
|
62
|
+
private cacheKey;
|
|
63
|
+
private labelKey;
|
|
64
|
+
/** Index an option under every language it carries, so any stored spelling finds it. */
|
|
65
|
+
private rememberLabels;
|
|
66
|
+
/** The texts a stored value could have been indexed under. */
|
|
67
|
+
private candidateTexts;
|
|
68
|
+
private invoke;
|
|
69
|
+
private toPromise;
|
|
70
|
+
private isSubscribable;
|
|
71
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<LookupRegistryService, never>;
|
|
72
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<LookupRegistryService>;
|
|
73
|
+
}
|
|
74
|
+
/** A choice field component, as far as option resolution is concerned. */
|
|
75
|
+
export interface ChoiceOptionHost {
|
|
76
|
+
readonly field: NestedFieldConfig | undefined;
|
|
77
|
+
readonly language: string;
|
|
78
|
+
readonly options: WritableSignal<DropdownOption[]>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Point a choice component's `options` signal at the right source (parity plan §6.3).
|
|
82
|
+
*
|
|
83
|
+
* Called from the `field` / `language` input setters of `dropdown`, `radio` and `multiSelect` —
|
|
84
|
+
* the wiring is theirs, the decision is the service's, which is what keeps the ADR-008
|
|
85
|
+
* five-input contract intact: options are never pushed in as a sixth input.
|
|
86
|
+
*
|
|
87
|
+
* Setters rather than `ngOnChanges` on purpose. `ngOnChanges` only fires for inputs Angular
|
|
88
|
+
* itself sets, so a component whose `field` is assigned directly — which the field specs do,
|
|
89
|
+
* and any consumer holding a `ComponentRef` may do — would render no options at all.
|
|
90
|
+
*
|
|
91
|
+
* Paints synchronously first (inline options, or a warm list), then fills in when a cold list
|
|
92
|
+
* lands. The late result is dropped if the field or language changed while it was in flight.
|
|
93
|
+
*/
|
|
94
|
+
export declare function refreshChoiceOptions(host: ChoiceOptionHost, lookups: LookupRegistryService): void;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EntityFormConfig, NestedFieldConfig, NestedTabConfig } from '@dynamic-entity/core';
|
|
2
2
|
import * as i0 from "@angular/core";
|
|
3
3
|
/**
|
|
4
4
|
* RbacService — ALL permission checks and masking logic live here.
|
|
5
|
-
* CRITICAL SYNC: resolveEffectiveMask() must be identical to rbac.utils.js in dynamic-entity-server.
|
|
6
|
-
* Changing one without the other is a critical production bug (ADR-003).
|
|
7
5
|
*/
|
|
8
6
|
export declare class RbacService {
|
|
9
7
|
private readonly maskedRoles;
|
|
@@ -12,17 +10,14 @@ export declare class RbacService {
|
|
|
12
10
|
* hasPermission(roles, undefined) → true (no restriction).
|
|
13
11
|
*/
|
|
14
12
|
hasPermission(userRoles: string[], requiredRoles?: string[]): boolean;
|
|
15
|
-
/**
|
|
16
|
-
* ADR-003: 3-level OR resolution.
|
|
17
|
-
* MUST match rbac.utils.js resolveEffectiveMask() exactly.
|
|
18
|
-
*/
|
|
13
|
+
/** 3-level OR resolution */
|
|
19
14
|
resolveEffectiveMask(formMask?: boolean, tabMask?: boolean, fieldMask?: boolean): boolean;
|
|
20
15
|
/** Check if the user's roles include any masked role */
|
|
21
16
|
isUserMaskedRole(userRoles: string[]): boolean;
|
|
22
17
|
/** Determine if a specific field should be masked for this user */
|
|
23
|
-
shouldMaskField(field:
|
|
18
|
+
shouldMaskField(field: NestedFieldConfig, tab: NestedTabConfig | undefined, config: EntityFormConfig, userRoles: string[]): boolean;
|
|
24
19
|
/** Get view/edit/delete permissions for a config */
|
|
25
|
-
getPermissions(config:
|
|
20
|
+
getPermissions(config: EntityFormConfig, userRoles: string[]): {
|
|
26
21
|
canView: boolean;
|
|
27
22
|
canEdit: boolean;
|
|
28
23
|
canDelete: boolean;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EntityFormConfig, FormRule, RuleEvaluationResult } from '@dynamic-entity/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
/**
|
|
4
|
+
* RulesEvaluationService — evaluates dynamic form rules against active reactive form state.
|
|
5
|
+
*/
|
|
6
|
+
export declare class RulesEvaluationService {
|
|
7
|
+
/**
|
|
8
|
+
* Evaluate rules against form values and optional original baseline values.
|
|
9
|
+
*/
|
|
10
|
+
evaluate(rules: FormRule[] | undefined | null, values: Record<string, unknown>, originalValues?: Record<string, unknown>): RuleEvaluationResult;
|
|
11
|
+
/**
|
|
12
|
+
* Filter rules pertinent to a single tab.
|
|
13
|
+
*/
|
|
14
|
+
filterForTab(rules: FormRule[] | undefined | null, tabId: string, config: EntityFormConfig): FormRule[];
|
|
15
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<RulesEvaluationService, never>;
|
|
16
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<RulesEvaluationService>;
|
|
17
|
+
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { ValidatorFn } from '@angular/forms';
|
|
2
|
+
import type { FieldValidators } from '@dynamic-entity/core';
|
|
2
3
|
import * as i0 from "@angular/core";
|
|
3
4
|
/**
|
|
4
|
-
* ValidatorRegistryService — resolves validator key strings to Angular ValidatorFns.
|
|
5
|
-
* Built-in validators: 'required', 'email', 'min:N', 'max:N', 'minLength:N', 'maxLength:N'.
|
|
6
|
-
* Consumers can register custom validators via provideNgxDynamicEntity({ validators: {...} }).
|
|
5
|
+
* ValidatorRegistryService — resolves validator configs / key strings to Angular ValidatorFns.
|
|
7
6
|
*/
|
|
8
7
|
export declare class ValidatorRegistryService {
|
|
9
8
|
private readonly consumerRegistry;
|
|
10
9
|
/**
|
|
11
|
-
* Resolve a validator key to a ValidatorFn.
|
|
10
|
+
* Resolve a validator key string to a ValidatorFn.
|
|
12
11
|
* Supports parameterized built-ins like 'min:0', 'maxLength:255'.
|
|
13
12
|
*/
|
|
14
13
|
resolve(validatorKey: string): ValidatorFn | null;
|
|
15
14
|
/** Resolve multiple validator keys into a ValidatorFn[] */
|
|
16
15
|
resolveAll(validatorKeys?: string[]): ValidatorFn[];
|
|
16
|
+
/** Resolve a FieldValidators object or string[] into ValidatorFn[] */
|
|
17
|
+
resolveFromConfig(config?: FieldValidators | string[]): ValidatorFn[];
|
|
17
18
|
static ɵfac: i0.ɵɵFactoryDeclaration<ValidatorRegistryService, never>;
|
|
18
19
|
static ɵprov: i0.ɵɵInjectableDeclaration<ValidatorRegistryService>;
|
|
19
20
|
}
|
|
@@ -1,16 +1,121 @@
|
|
|
1
1
|
import { InjectionToken } from '@angular/core';
|
|
2
2
|
import type { Type } from '@angular/core';
|
|
3
|
-
|
|
4
|
-
export declare const DYNAMIC_ENTITY_API_URL: InjectionToken<string>;
|
|
3
|
+
import { type EntityRefCacheStore } from '../services/entity-ref-cache';
|
|
5
4
|
/** Roles that see XXXXXXXXX for masked fields (ADR-003) */
|
|
6
5
|
export declare const MASKED_ROLES: InjectionToken<string[]>;
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Consumer **overrides**: fieldType string → Angular component class.
|
|
8
|
+
* Highest priority — beats anything registered through `provideFieldTypes()`.
|
|
9
|
+
*/
|
|
10
10
|
export declare const FIELD_TYPE_REGISTRY: InjectionToken<Map<string, Type<any>>>;
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Multi-provider of field-type sets, contributed by `provideFieldTypes()` /
|
|
13
|
+
* `provideBuiltInFieldTypes()`. Multi so several calls compose instead of clobbering;
|
|
14
|
+
* later sets win on key collision. This is the seam that keeps unused field components
|
|
15
|
+
* out of the bundle — nothing is imported until a set names it.
|
|
16
|
+
*/
|
|
17
|
+
export declare const FIELD_TYPE_SETS: InjectionToken<Record<string, Type<any>>[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Registry: entity key → option loader.
|
|
20
|
+
*
|
|
21
|
+
* A loader receives a `ReferenceLoaderContext` (`parentValue`, `filters`, `lang`) and may
|
|
22
|
+
* return an array, a Promise, or an Observable of `ReferenceOption[]`. `ctx` is optional,
|
|
23
|
+
* so a zero-arg loader (`() => svc.list()`) is still a valid registration.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ENTITY_REF_REGISTRY: InjectionToken<Map<string, EntityReferenceLoader>>;
|
|
26
|
+
/**
|
|
27
|
+
* Registry: list name → the values of a named master list (`field.listName`).
|
|
28
|
+
*
|
|
29
|
+
* A source may be the values themselves (array / Promise / Observable) or a **loader function**,
|
|
30
|
+
* which is what you want for anything fetched: a bare Promise is created when the provider is
|
|
31
|
+
* built, so every list would load whether or not a form uses it.
|
|
32
|
+
*
|
|
33
|
+
* Values are normalised by `normalizeLookupValues` — bare strings and `LocalizedText` are
|
|
34
|
+
* accepted alongside the full `{ code, name, sortOrder }` shape.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* provideNgxDynamicEntity({
|
|
38
|
+
* lookups: {
|
|
39
|
+
* employeeStatus: () => listService.getByName('employeeStatus'),
|
|
40
|
+
* grades: ['Junior', 'Senior'],
|
|
41
|
+
* },
|
|
42
|
+
* })
|
|
43
|
+
*/
|
|
44
|
+
export declare const LOOKUP_REGISTRY: InjectionToken<Map<string, LookupListSource>>;
|
|
45
|
+
/**
|
|
46
|
+
* Ordered steps that move a saved record forward when a config's `version` increases.
|
|
47
|
+
*
|
|
48
|
+
* Without these, `EntityFormConfig.version` and `VersionedRecord._configVersion` are
|
|
49
|
+
* declarations nothing acts on: a schema can be edited freely while records keep their old
|
|
50
|
+
* shape, and nothing reconciles the two. Registering migrations makes the version numbers
|
|
51
|
+
* mean something.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* provideNgxDynamicEntity({
|
|
55
|
+
* migrations: [
|
|
56
|
+
* { from: 1, to: 2, description: 'split name', migrate: r => ({ ...r, firstName: r['name'] }) },
|
|
57
|
+
* ],
|
|
58
|
+
* })
|
|
59
|
+
*/
|
|
60
|
+
export declare const RECORD_MIGRATIONS: InjectionToken<RecordMigration[]>;
|
|
13
61
|
/** Registry: validator key → ValidatorFn */
|
|
14
62
|
export declare const VALIDATOR_REGISTRY: InjectionToken<Map<string, any>>;
|
|
63
|
+
/**
|
|
64
|
+
* A registered hook: receives the payload, returns it (or a replacement), sync or async.
|
|
65
|
+
*
|
|
66
|
+
* Spelled out rather than typed as `Function`, which accepts class declarations and any
|
|
67
|
+
* arity and gives a consumer no signature to write against.
|
|
68
|
+
*/
|
|
69
|
+
export type HookFn = (data: any, context?: unknown) => unknown | Promise<unknown>;
|
|
15
70
|
/** Registry: hook key → async (data, context) => data */
|
|
16
|
-
export declare const HOOK_REGISTRY: InjectionToken<Map<string,
|
|
71
|
+
export declare const HOOK_REGISTRY: InjectionToken<Map<string, HookFn>>;
|
|
72
|
+
/**
|
|
73
|
+
* Consumer-registered list of common module tabs (documents, audit, tasks, etc.).
|
|
74
|
+
* The library ships no built-in implementations. Consumers provide their own list
|
|
75
|
+
* so the builder's module-picker and the renderer's tab switcher know what's available.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* providers: [
|
|
79
|
+
* {
|
|
80
|
+
* provide: COMMON_MODULES_REGISTRY,
|
|
81
|
+
* useValue: [
|
|
82
|
+
* { id: 'documents', label: { en: 'Documents' }, component: 'app-documents-view' },
|
|
83
|
+
* { id: 'audit', label: { en: 'Audit Log' }, component: 'app-audit' },
|
|
84
|
+
* ] satisfies CommonModuleEntry[]
|
|
85
|
+
* }
|
|
86
|
+
* ]
|
|
87
|
+
*/
|
|
88
|
+
export declare const COMMON_MODULES_REGISTRY: InjectionToken<CommonModuleEntry[]>;
|
|
89
|
+
/**
|
|
90
|
+
* Consumer-provided file/image upload handler.
|
|
91
|
+
* When supplied, image-field and file-field call it to persist the File and receive a stable
|
|
92
|
+
* URL back. Without it the field stores `{ file, name, size, mimeType }` for the consumer to
|
|
93
|
+
* upload on submit. May return the result directly, as a Promise, or as an Observable.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* { provide: UPLOAD_HANDLER, useFactory: () => (file: File) => myUploadService.upload(file) }
|
|
97
|
+
*/
|
|
98
|
+
export declare const UPLOAD_HANDLER: InjectionToken<FileUploadHandler>;
|
|
99
|
+
/**
|
|
100
|
+
* Backing store for the entity-reference options cache. Defaults to in-memory.
|
|
101
|
+
* Provide your own to make the cache outlive a page refresh.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* { provide: ENTITY_REF_CACHE_STORE, useClass: SessionStorageRefCache }
|
|
105
|
+
*/
|
|
106
|
+
export declare const ENTITY_REF_CACHE_STORE: InjectionToken<EntityRefCacheStore>;
|
|
107
|
+
/**
|
|
108
|
+
* Consumer-provided predicate function to check whether user roles can edit system default tabs or fields.
|
|
109
|
+
* Defaults to allowing edits if not provided.
|
|
110
|
+
*/
|
|
111
|
+
/**
|
|
112
|
+
* Consumer-provided predicate function to check whether user roles can edit system default tabs or fields.
|
|
113
|
+
* Defaults to allowing edits if not provided.
|
|
114
|
+
*/
|
|
115
|
+
export declare const SYSTEM_DEFAULT_CAN_EDIT: InjectionToken<(roles: string[]) => boolean>;
|
|
116
|
+
export type ConfigSourceHandler = (entityKey: string) => any;
|
|
117
|
+
/**
|
|
118
|
+
* Registry/Resolver for resolving an EntityFormConfig by entity key across entities (Phase 8).
|
|
119
|
+
* Enables cross-entity field referencing (`isReferenced`, `referencedEntityKey`, `referencedFieldId`, `hasDrift`).
|
|
120
|
+
*/
|
|
121
|
+
export declare const CONFIG_SOURCE: InjectionToken<ConfigSourceHandler>;
|
package/package.json
CHANGED
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ngx-dynamic-entity",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Angular
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Angular standalone UI library for rendering dynamic entity forms.",
|
|
5
|
+
"//typings": "Local workspace resolution only: ng-packagr overwrites this with the correct in-tarball path ('index.d.ts') when generating dist/package.json. Do NOT add a 'main' field here — ng-packagr does not generate one, so it would be copied verbatim and resolve to dist/dist/... in the published package.",
|
|
6
|
+
"typings": "index.d.ts",
|
|
5
7
|
"peerDependencies": {
|
|
6
|
-
"@angular/common": "^17.0.0",
|
|
7
|
-
"@angular/core": "^17.0.0",
|
|
8
|
-
"@angular/forms": "^17.0.0",
|
|
9
|
-
"@dynamic-entity/core": "
|
|
8
|
+
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
|
|
9
|
+
"@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
|
|
10
|
+
"@angular/forms": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
|
|
11
|
+
"@dynamic-entity/core": "^1.1.0",
|
|
10
12
|
"rxjs": "^7.0.0"
|
|
11
13
|
},
|
|
12
14
|
"keywords": [
|
|
13
15
|
"angular",
|
|
14
16
|
"dynamic-entity",
|
|
15
17
|
"dynamic-forms",
|
|
16
|
-
"dynamic-table",
|
|
17
18
|
"rbac"
|
|
18
19
|
],
|
|
19
20
|
"license": "MIT",
|
|
20
21
|
"sideEffects": false,
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/berserker5619/Dynamic-Entity.git",
|
|
25
|
+
"directory": "packages/ngx-dynamic-entity"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/berserker5619/Dynamic-Entity/tree/main/packages/ngx-dynamic-entity#readme",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/berserker5619/Dynamic-Entity/issues"
|
|
30
|
+
},
|
|
31
|
+
"author": "Nizamudeen",
|
|
21
32
|
"module": "fesm2022/ngx-dynamic-entity.mjs",
|
|
22
|
-
"typings": "index.d.ts",
|
|
23
33
|
"exports": {
|
|
24
34
|
"./package.json": {
|
|
25
35
|
"default": "./package.json"
|
package/public-api.d.ts
CHANGED
|
@@ -1,21 +1,38 @@
|
|
|
1
1
|
export * from './lib/tokens/injection-tokens';
|
|
2
2
|
export * from './lib/providers/provide-ngx-dynamic-entity';
|
|
3
|
-
export * from './lib/
|
|
3
|
+
export * from './lib/providers/provide-field-types';
|
|
4
4
|
export * from './lib/services/field-registry.service';
|
|
5
5
|
export * from './lib/services/hook-registry.service';
|
|
6
6
|
export * from './lib/services/validator-registry.service';
|
|
7
7
|
export * from './lib/services/entity-ref-registry.service';
|
|
8
|
-
export * from './lib/services/
|
|
8
|
+
export * from './lib/services/entity-ref-selection.service';
|
|
9
|
+
export * from './lib/services/cascade-data.service';
|
|
10
|
+
export * from './lib/services/entity-reference.service';
|
|
11
|
+
export * from './lib/services/entity-ref-cache';
|
|
12
|
+
export * from './lib/services/lookup-registry.service';
|
|
9
13
|
export * from './lib/services/rbac.service';
|
|
14
|
+
export * from './lib/services/rules-evaluation.service';
|
|
15
|
+
export * from './lib/services/config-source.service';
|
|
10
16
|
export * from './lib/form/dynamic-form.component';
|
|
17
|
+
export * from './lib/form/dynamic-record-form.component';
|
|
11
18
|
export * from './lib/form/dynamic-field/dynamic-field.component';
|
|
12
|
-
export * from './lib/table/dynamic-table.component';
|
|
13
19
|
export * from './lib/field-types/text-field.component';
|
|
14
20
|
export * from './lib/field-types/textarea-field.component';
|
|
15
21
|
export * from './lib/field-types/number-field.component';
|
|
22
|
+
export * from './lib/field-types/currency-field.component';
|
|
23
|
+
export * from './lib/field-types/email-field.component';
|
|
24
|
+
export * from './lib/field-types/password-field.component';
|
|
16
25
|
export * from './lib/field-types/checkbox-field.component';
|
|
26
|
+
export * from './lib/field-types/boolean-field.component';
|
|
17
27
|
export * from './lib/field-types/date-field.component';
|
|
28
|
+
export * from './lib/field-types/month-year-field.component';
|
|
18
29
|
export * from './lib/field-types/dropdown-field.component';
|
|
30
|
+
export * from './lib/field-types/radio-field.component';
|
|
19
31
|
export * from './lib/field-types/multi-select-field.component';
|
|
20
32
|
export * from './lib/field-types/entity-ref-field.component';
|
|
21
|
-
export
|
|
33
|
+
export * from './lib/field-types/group-field.component';
|
|
34
|
+
export * from './lib/field-types/array-field.component';
|
|
35
|
+
export * from './lib/field-types/image-field.component';
|
|
36
|
+
export * from './lib/field-types/file-field.component';
|
|
37
|
+
export { resolveLabel, resolveOptionLabel, resolveOptionValue, formatDisplayValue, valuesMatch, normalizeOption, normalizeConfig, } from '@dynamic-entity/core';
|
|
38
|
+
export type { EntityFormConfig, NestedTabConfig, NestedFieldConfig, FieldTableConfig, RichFieldType, LocalizedText, DropdownOption, VersionedRecord, EntityPermissions, RbacContext, FieldValidators, EntityReferenceConfig, EntityReferenceLoader, ReferenceOption, ReferenceLoaderContext, AutoPatchConfig, PatchOnTrueMapping, FileRef, FormRule, RuleCondition, RuleAction, RuleEvaluationResult, CommonModuleEntry, } from '@dynamic-entity/core';
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { Injectable, inject } from '@angular/core';
|
|
2
|
-
import { HttpClient } from '@angular/common/http';
|
|
3
|
-
import { map, shareReplay } from 'rxjs/operators';
|
|
4
|
-
import { DYNAMIC_ENTITY_API_URL } from '../tokens/injection-tokens';
|
|
5
|
-
import * as i0 from "@angular/core";
|
|
6
|
-
/**
|
|
7
|
-
* ConfigService — fetches entity configs from the dynamic-entity-server API.
|
|
8
|
-
* Uses shareReplay(1) to cache per config key — avoids duplicate HTTP requests.
|
|
9
|
-
* Following ONEHERMES smart service pattern: Observable$ + shareReplay.
|
|
10
|
-
*/
|
|
11
|
-
export class ConfigService {
|
|
12
|
-
http = inject(HttpClient);
|
|
13
|
-
apiUrl = inject(DYNAMIC_ENTITY_API_URL, { optional: true }) ?? '';
|
|
14
|
-
cache = new Map();
|
|
15
|
-
/** Get a config by entity key. Cached via shareReplay(1). */
|
|
16
|
-
getConfig(entity) {
|
|
17
|
-
if (!this.cache.has(entity)) {
|
|
18
|
-
const config$ = this.http
|
|
19
|
-
.get(`${this.apiUrl}/config/${entity}`)
|
|
20
|
-
.pipe(map(res => res.data), // ADR-004: Success guarantees data
|
|
21
|
-
shareReplay(1));
|
|
22
|
-
this.cache.set(entity, config$);
|
|
23
|
-
}
|
|
24
|
-
return this.cache.get(entity);
|
|
25
|
-
}
|
|
26
|
-
/** List all configs */
|
|
27
|
-
listConfigs() {
|
|
28
|
-
return this.http
|
|
29
|
-
.get(`${this.apiUrl}/config`)
|
|
30
|
-
.pipe(map(res => res.data)); // ADR-004: Success guarantees data
|
|
31
|
-
}
|
|
32
|
-
/** Create a new config */
|
|
33
|
-
saveConfig(config) {
|
|
34
|
-
return this.http
|
|
35
|
-
.post(`${this.apiUrl}/config`, config)
|
|
36
|
-
.pipe(map(res => res.data));
|
|
37
|
-
}
|
|
38
|
-
/** Update an existing config */
|
|
39
|
-
updateConfig(entity, updates) {
|
|
40
|
-
return this.http
|
|
41
|
-
.put(`${this.apiUrl}/config/${entity}`, updates)
|
|
42
|
-
.pipe(map(res => {
|
|
43
|
-
this.invalidate(entity);
|
|
44
|
-
return res.data;
|
|
45
|
-
}));
|
|
46
|
-
}
|
|
47
|
-
/** Invalidate cache for an entity (call after config update) */
|
|
48
|
-
invalidate(entity) {
|
|
49
|
-
this.cache.delete(entity);
|
|
50
|
-
}
|
|
51
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
52
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ConfigService, providedIn: 'root' });
|
|
53
|
-
}
|
|
54
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ConfigService, decorators: [{
|
|
55
|
-
type: Injectable,
|
|
56
|
-
args: [{ providedIn: 'root' }]
|
|
57
|
-
}] });
|
|
58
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLnNlcnZpY2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvbGliL3NlcnZpY2VzL2NvbmZpZy5zZXJ2aWNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxFQUFFLE1BQU0sZUFBZSxDQUFDO0FBQ25ELE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUVsRCxPQUFPLEVBQUUsR0FBRyxFQUFFLFdBQVcsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBRWxELE9BQU8sRUFBRSxzQkFBc0IsRUFBRSxNQUFNLDRCQUE0QixDQUFDOztBQUVwRTs7OztHQUlHO0FBRUgsTUFBTSxPQUFPLGFBQWE7SUFDUCxJQUFJLEdBQUcsTUFBTSxDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQzFCLE1BQU0sR0FBRyxNQUFNLENBQUMsc0JBQXNCLEVBQUUsRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLENBQUMsSUFBSSxFQUFFLENBQUM7SUFDbEUsS0FBSyxHQUFHLElBQUksR0FBRyxFQUFvQyxDQUFDO0lBRXJFLDZEQUE2RDtJQUM3RCxTQUFTLENBQUMsTUFBYztRQUN0QixJQUFJLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztZQUM1QixNQUFNLE9BQU8sR0FBRyxJQUFJLENBQUMsSUFBSTtpQkFDdEIsR0FBRyxDQUEyQyxHQUFHLElBQUksQ0FBQyxNQUFNLFdBQVcsTUFBTSxFQUFFLENBQUM7aUJBQ2hGLElBQUksQ0FDSCxHQUFHLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxHQUFHLENBQUMsSUFBSyxDQUFDLEVBQUUsbUNBQW1DO1lBQzFELFdBQVcsQ0FBQyxDQUFDLENBQUMsQ0FDZixDQUFDO1lBQ0osSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxDQUFDO1FBQ2xDLENBQUM7UUFDRCxPQUFPLElBQUksQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBRSxDQUFDO0lBQ2pDLENBQUM7SUFFRCx1QkFBdUI7SUFDdkIsV0FBVztRQUNULE9BQU8sSUFBSSxDQUFDLElBQUk7YUFDYixHQUFHLENBQTZDLEdBQUcsSUFBSSxDQUFDLE1BQU0sU0FBUyxDQUFDO2FBQ3hFLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxHQUFHLENBQUMsSUFBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLG1DQUFtQztJQUNyRSxDQUFDO0lBRUQsMEJBQTBCO0lBQzFCLFVBQVUsQ0FBQyxNQUFvQjtRQUM3QixPQUFPLElBQUksQ0FBQyxJQUFJO2FBQ2IsSUFBSSxDQUEyQyxHQUFHLElBQUksQ0FBQyxNQUFNLFNBQVMsRUFBRSxNQUFNLENBQUM7YUFDL0UsSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLEdBQUcsQ0FBQyxJQUFLLENBQUMsQ0FBQyxDQUFDO0lBQ2pDLENBQUM7SUFFRCxnQ0FBZ0M7SUFDaEMsWUFBWSxDQUFDLE1BQWMsRUFBRSxPQUE4QjtRQUN6RCxPQUFPLElBQUksQ0FBQyxJQUFJO2FBQ2IsR0FBRyxDQUEyQyxHQUFHLElBQUksQ0FBQyxNQUFNLFdBQVcsTUFBTSxFQUFFLEVBQUUsT0FBTyxDQUFDO2FBQ3pGLElBQUksQ0FDSCxHQUFHLENBQUMsR0FBRyxDQUFDLEVBQUU7WUFDUixJQUFJLENBQUMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxDQUFDO1lBQ3hCLE9BQU8sR0FBRyxDQUFDLElBQUssQ0FBQztRQUNuQixDQUFDLENBQUMsQ0FDSCxDQUFDO0lBQ04sQ0FBQztJQUVELGdFQUFnRTtJQUNoRSxVQUFVLENBQUMsTUFBYztRQUN2QixJQUFJLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUM1QixDQUFDO3dHQWhEVSxhQUFhOzRHQUFiLGFBQWEsY0FEQSxNQUFNOzs0RkFDbkIsYUFBYTtrQkFEekIsVUFBVTttQkFBQyxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBJbmplY3RhYmxlLCBpbmplY3QgfSBmcm9tICdAYW5ndWxhci9jb3JlJztcbmltcG9ydCB7IEh0dHBDbGllbnQgfSBmcm9tICdAYW5ndWxhci9jb21tb24vaHR0cCc7XG5pbXBvcnQgeyBPYnNlcnZhYmxlIH0gZnJvbSAncnhqcyc7XG5pbXBvcnQgeyBtYXAsIHNoYXJlUmVwbGF5IH0gZnJvbSAncnhqcy9vcGVyYXRvcnMnO1xuaW1wb3J0IHR5cGUgeyBFbnRpdHlDb25maWcgfSBmcm9tICdAZHluYW1pYy1lbnRpdHkvY29yZSc7XG5pbXBvcnQgeyBEWU5BTUlDX0VOVElUWV9BUElfVVJMIH0gZnJvbSAnLi4vdG9rZW5zL2luamVjdGlvbi10b2tlbnMnO1xuXG4vKipcbiAqIENvbmZpZ1NlcnZpY2Ug4oCUIGZldGNoZXMgZW50aXR5IGNvbmZpZ3MgZnJvbSB0aGUgZHluYW1pYy1lbnRpdHktc2VydmVyIEFQSS5cbiAqIFVzZXMgc2hhcmVSZXBsYXkoMSkgdG8gY2FjaGUgcGVyIGNvbmZpZyBrZXkg4oCUIGF2b2lkcyBkdXBsaWNhdGUgSFRUUCByZXF1ZXN0cy5cbiAqIEZvbGxvd2luZyBPTkVIRVJNRVMgc21hcnQgc2VydmljZSBwYXR0ZXJuOiBPYnNlcnZhYmxlJCArIHNoYXJlUmVwbGF5LlxuICovXG5ASW5qZWN0YWJsZSh7IHByb3ZpZGVkSW46ICdyb290JyB9KVxuZXhwb3J0IGNsYXNzIENvbmZpZ1NlcnZpY2Uge1xuICBwcml2YXRlIHJlYWRvbmx5IGh0dHAgPSBpbmplY3QoSHR0cENsaWVudCk7XG4gIHByaXZhdGUgcmVhZG9ubHkgYXBpVXJsID0gaW5qZWN0KERZTkFNSUNfRU5USVRZX0FQSV9VUkwsIHsgb3B0aW9uYWw6IHRydWUgfSkgPz8gJyc7XG4gIHByaXZhdGUgcmVhZG9ubHkgY2FjaGUgPSBuZXcgTWFwPHN0cmluZywgT2JzZXJ2YWJsZTxFbnRpdHlDb25maWc+PigpO1xuXG4gIC8qKiBHZXQgYSBjb25maWcgYnkgZW50aXR5IGtleS4gQ2FjaGVkIHZpYSBzaGFyZVJlcGxheSgxKS4gKi9cbiAgZ2V0Q29uZmlnKGVudGl0eTogc3RyaW5nKTogT2JzZXJ2YWJsZTxFbnRpdHlDb25maWc+IHtcbiAgICBpZiAoIXRoaXMuY2FjaGUuaGFzKGVudGl0eSkpIHtcbiAgICAgIGNvbnN0IGNvbmZpZyQgPSB0aGlzLmh0dHBcbiAgICAgICAgLmdldDx7IHN1Y2Nlc3M6IGJvb2xlYW47IGRhdGE6IEVudGl0eUNvbmZpZyB9PihgJHt0aGlzLmFwaVVybH0vY29uZmlnLyR7ZW50aXR5fWApXG4gICAgICAgIC5waXBlKFxuICAgICAgICAgIG1hcChyZXMgPT4gcmVzLmRhdGEhKSwgLy8gQURSLTAwNDogU3VjY2VzcyBndWFyYW50ZWVzIGRhdGFcbiAgICAgICAgICBzaGFyZVJlcGxheSgxKSxcbiAgICAgICAgKTtcbiAgICAgIHRoaXMuY2FjaGUuc2V0KGVudGl0eSwgY29uZmlnJCk7XG4gICAgfVxuICAgIHJldHVybiB0aGlzLmNhY2hlLmdldChlbnRpdHkpITtcbiAgfVxuXG4gIC8qKiBMaXN0IGFsbCBjb25maWdzICovXG4gIGxpc3RDb25maWdzKCk6IE9ic2VydmFibGU8RW50aXR5Q29uZmlnW10+IHtcbiAgICByZXR1cm4gdGhpcy5odHRwXG4gICAgICAuZ2V0PHsgc3VjY2VzczogYm9vbGVhbjsgZGF0YTogRW50aXR5Q29uZmlnW10gfT4oYCR7dGhpcy5hcGlVcmx9L2NvbmZpZ2ApXG4gICAgICAucGlwZShtYXAocmVzID0+IHJlcy5kYXRhISkpOyAvLyBBRFItMDA0OiBTdWNjZXNzIGd1YXJhbnRlZXMgZGF0YVxuICB9XG5cbiAgLyoqIENyZWF0ZSBhIG5ldyBjb25maWcgKi9cbiAgc2F2ZUNvbmZpZyhjb25maWc6IEVudGl0eUNvbmZpZyk6IE9ic2VydmFibGU8RW50aXR5Q29uZmlnPiB7XG4gICAgcmV0dXJuIHRoaXMuaHR0cFxuICAgICAgLnBvc3Q8eyBzdWNjZXNzOiBib29sZWFuOyBkYXRhOiBFbnRpdHlDb25maWcgfT4oYCR7dGhpcy5hcGlVcmx9L2NvbmZpZ2AsIGNvbmZpZylcbiAgICAgIC5waXBlKG1hcChyZXMgPT4gcmVzLmRhdGEhKSk7XG4gIH1cblxuICAvKiogVXBkYXRlIGFuIGV4aXN0aW5nIGNvbmZpZyAqL1xuICB1cGRhdGVDb25maWcoZW50aXR5OiBzdHJpbmcsIHVwZGF0ZXM6IFBhcnRpYWw8RW50aXR5Q29uZmlnPik6IE9ic2VydmFibGU8RW50aXR5Q29uZmlnPiB7XG4gICAgcmV0dXJuIHRoaXMuaHR0cFxuICAgICAgLnB1dDx7IHN1Y2Nlc3M6IGJvb2xlYW47IGRhdGE6IEVudGl0eUNvbmZpZyB9PihgJHt0aGlzLmFwaVVybH0vY29uZmlnLyR7ZW50aXR5fWAsIHVwZGF0ZXMpXG4gICAgICAucGlwZShcbiAgICAgICAgbWFwKHJlcyA9PiB7XG4gICAgICAgICAgdGhpcy5pbnZhbGlkYXRlKGVudGl0eSk7XG4gICAgICAgICAgcmV0dXJuIHJlcy5kYXRhITtcbiAgICAgICAgfSlcbiAgICAgICk7XG4gIH1cblxuICAvKiogSW52YWxpZGF0ZSBjYWNoZSBmb3IgYW4gZW50aXR5IChjYWxsIGFmdGVyIGNvbmZpZyB1cGRhdGUpICovXG4gIGludmFsaWRhdGUoZW50aXR5OiBzdHJpbmcpOiB2b2lkIHtcbiAgICB0aGlzLmNhY2hlLmRlbGV0ZShlbnRpdHkpO1xuICB9XG59XG4iXX0=
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { Injectable, inject } from '@angular/core';
|
|
2
|
-
import { MIGRATION_STRATEGY } from '../tokens/injection-tokens';
|
|
3
|
-
import * as i0 from "@angular/core";
|
|
4
|
-
/**
|
|
5
|
-
* VersionService — checks config version staleness and migration strategy.
|
|
6
|
-
* ADR-005: Frontend strategy is 'strict' | 'graceful' only. Never 'auto'.
|
|
7
|
-
*/
|
|
8
|
-
export class VersionService {
|
|
9
|
-
strategy = inject(MIGRATION_STRATEGY, { optional: true }) ?? 'graceful';
|
|
10
|
-
/**
|
|
11
|
-
* Check if a record is stale compared to the current config version.
|
|
12
|
-
* Returns true if the record needs migration.
|
|
13
|
-
*/
|
|
14
|
-
needsMigration(record, config) {
|
|
15
|
-
if (!record._configVersion)
|
|
16
|
-
return false;
|
|
17
|
-
return record._configVersion < config.version || !!record._needsMigration;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Get the effective migration strategy for frontend use.
|
|
21
|
-
* Only 'strict' or 'graceful' — never 'auto' (ADR-005).
|
|
22
|
-
*/
|
|
23
|
-
getStrategy() {
|
|
24
|
-
return this.strategy === 'strict' ? 'strict' : 'graceful';
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Determine if form submission should be blocked for a stale record.
|
|
28
|
-
* 'strict' → block; 'graceful' → allow with warning.
|
|
29
|
-
*/
|
|
30
|
-
shouldBlockSubmit(record, config) {
|
|
31
|
-
return this.getStrategy() === 'strict' && this.needsMigration(record, config);
|
|
32
|
-
}
|
|
33
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: VersionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
34
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: VersionService, providedIn: 'root' });
|
|
35
|
-
}
|
|
36
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: VersionService, decorators: [{
|
|
37
|
-
type: Injectable,
|
|
38
|
-
args: [{ providedIn: 'root' }]
|
|
39
|
-
}] });
|
|
40
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmVyc2lvbi5zZXJ2aWNlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL2xpYi9zZXJ2aWNlcy92ZXJzaW9uLnNlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsTUFBTSxlQUFlLENBQUM7QUFFbkQsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sNEJBQTRCLENBQUM7O0FBRWhFOzs7R0FHRztBQUVILE1BQU0sT0FBTyxjQUFjO0lBQ1IsUUFBUSxHQUFHLE1BQU0sQ0FBQyxrQkFBa0IsRUFBRSxFQUFFLFFBQVEsRUFBRSxJQUFJLEVBQUUsQ0FBQyxJQUFJLFVBQVUsQ0FBQztJQUV6Rjs7O09BR0c7SUFDSCxjQUFjLENBQUMsTUFBZ0MsRUFBRSxNQUFvQjtRQUNuRSxJQUFJLENBQUMsTUFBTSxDQUFDLGNBQWM7WUFBRSxPQUFPLEtBQUssQ0FBQztRQUN6QyxPQUFPLE1BQU0sQ0FBQyxjQUFjLEdBQUcsTUFBTSxDQUFDLE9BQU8sSUFBSSxDQUFDLENBQUMsTUFBTSxDQUFDLGVBQWUsQ0FBQztJQUM1RSxDQUFDO0lBRUQ7OztPQUdHO0lBQ0gsV0FBVztRQUNULE9BQU8sSUFBSSxDQUFDLFFBQVEsS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsVUFBVSxDQUFDO0lBQzVELENBQUM7SUFFRDs7O09BR0c7SUFDSCxpQkFBaUIsQ0FBQyxNQUFnQyxFQUFFLE1BQW9CO1FBQ3RFLE9BQU8sSUFBSSxDQUFDLFdBQVcsRUFBRSxLQUFLLFFBQVEsSUFBSSxJQUFJLENBQUMsY0FBYyxDQUFDLE1BQU0sRUFBRSxNQUFNLENBQUMsQ0FBQztJQUNoRixDQUFDO3dHQTFCVSxjQUFjOzRHQUFkLGNBQWMsY0FERCxNQUFNOzs0RkFDbkIsY0FBYztrQkFEMUIsVUFBVTttQkFBQyxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBJbmplY3RhYmxlLCBpbmplY3QgfSBmcm9tICdAYW5ndWxhci9jb3JlJztcbmltcG9ydCB0eXBlIHsgVmVyc2lvbmVkUmVjb3JkLCBFbnRpdHlDb25maWcsIE1pZ3JhdGlvblN0cmF0ZWd5IH0gZnJvbSAnQGR5bmFtaWMtZW50aXR5L2NvcmUnO1xuaW1wb3J0IHsgTUlHUkFUSU9OX1NUUkFURUdZIH0gZnJvbSAnLi4vdG9rZW5zL2luamVjdGlvbi10b2tlbnMnO1xuXG4vKipcbiAqIFZlcnNpb25TZXJ2aWNlIOKAlCBjaGVja3MgY29uZmlnIHZlcnNpb24gc3RhbGVuZXNzIGFuZCBtaWdyYXRpb24gc3RyYXRlZ3kuXG4gKiBBRFItMDA1OiBGcm9udGVuZCBzdHJhdGVneSBpcyAnc3RyaWN0JyB8ICdncmFjZWZ1bCcgb25seS4gTmV2ZXIgJ2F1dG8nLlxuICovXG5ASW5qZWN0YWJsZSh7IHByb3ZpZGVkSW46ICdyb290JyB9KVxuZXhwb3J0IGNsYXNzIFZlcnNpb25TZXJ2aWNlIHtcbiAgcHJpdmF0ZSByZWFkb25seSBzdHJhdGVneSA9IGluamVjdChNSUdSQVRJT05fU1RSQVRFR1ksIHsgb3B0aW9uYWw6IHRydWUgfSkgPz8gJ2dyYWNlZnVsJztcblxuICAvKipcbiAgICogQ2hlY2sgaWYgYSByZWNvcmQgaXMgc3RhbGUgY29tcGFyZWQgdG8gdGhlIGN1cnJlbnQgY29uZmlnIHZlcnNpb24uXG4gICAqIFJldHVybnMgdHJ1ZSBpZiB0aGUgcmVjb3JkIG5lZWRzIG1pZ3JhdGlvbi5cbiAgICovXG4gIG5lZWRzTWlncmF0aW9uKHJlY29yZDogUGFydGlhbDxWZXJzaW9uZWRSZWNvcmQ+LCBjb25maWc6IEVudGl0eUNvbmZpZyk6IGJvb2xlYW4ge1xuICAgIGlmICghcmVjb3JkLl9jb25maWdWZXJzaW9uKSByZXR1cm4gZmFsc2U7XG4gICAgcmV0dXJuIHJlY29yZC5fY29uZmlnVmVyc2lvbiA8IGNvbmZpZy52ZXJzaW9uIHx8ICEhcmVjb3JkLl9uZWVkc01pZ3JhdGlvbjtcbiAgfVxuXG4gIC8qKlxuICAgKiBHZXQgdGhlIGVmZmVjdGl2ZSBtaWdyYXRpb24gc3RyYXRlZ3kgZm9yIGZyb250ZW5kIHVzZS5cbiAgICogT25seSAnc3RyaWN0JyBvciAnZ3JhY2VmdWwnIOKAlCBuZXZlciAnYXV0bycgKEFEUi0wMDUpLlxuICAgKi9cbiAgZ2V0U3RyYXRlZ3koKTogJ3N0cmljdCcgfCAnZ3JhY2VmdWwnIHtcbiAgICByZXR1cm4gdGhpcy5zdHJhdGVneSA9PT0gJ3N0cmljdCcgPyAnc3RyaWN0JyA6ICdncmFjZWZ1bCc7XG4gIH1cblxuICAvKipcbiAgICogRGV0ZXJtaW5lIGlmIGZvcm0gc3VibWlzc2lvbiBzaG91bGQgYmUgYmxvY2tlZCBmb3IgYSBzdGFsZSByZWNvcmQuXG4gICAqICdzdHJpY3QnIOKGkiBibG9jazsgJ2dyYWNlZnVsJyDihpIgYWxsb3cgd2l0aCB3YXJuaW5nLlxuICAgKi9cbiAgc2hvdWxkQmxvY2tTdWJtaXQocmVjb3JkOiBQYXJ0aWFsPFZlcnNpb25lZFJlY29yZD4sIGNvbmZpZzogRW50aXR5Q29uZmlnKTogYm9vbGVhbiB7XG4gICAgcmV0dXJuIHRoaXMuZ2V0U3RyYXRlZ3koKSA9PT0gJ3N0cmljdCcgJiYgdGhpcy5uZWVkc01pZ3JhdGlvbihyZWNvcmQsIGNvbmZpZyk7XG4gIH1cbn1cbiJdfQ==
|