@docupace/ihub-config 1.1.17 → 1.1.18
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 +182 -182
- package/dist/config-service.d.ts +9 -1
- package/dist/config-service.d.ts.map +1 -1
- package/dist/config-service.js +198 -4
- package/dist/config-service.js.map +1 -1
- package/dist/model/graphs/config/common.graphql +7 -7
- package/dist/model/graphs/config/fields.graphql +379 -367
- package/dist/model/graphs/config/menu.graphql +54 -54
- package/dist/model/graphs/config/mutation.graphql +30 -30
- package/dist/model/graphs/config/page.graphql +256 -256
- package/dist/model/graphs/config/panel.graphql +30 -30
- package/dist/model/graphs/config/query.graphql +84 -78
- package/dist/model/graphs/config/rules.graphql +80 -0
- package/dist/model/graphs/config/selectOptions.graphql +47 -47
- package/dist/model/graphs/config/table.graphql +182 -182
- package/dist/model/graphs/config/tests.graphql +16 -16
- package/dist/model/graphs/config/theme.graphql +41 -41
- package/dist/model/graphs/config/ui-components.graphql +10 -10
- package/dist/model/graphs/domains/scalars.graphql +3 -3
- package/dist/model/graphs/filter.graphql +5 -5
- package/dist/model/graphs/schema.graphql +7 -7
- package/model/graphs/config/common.graphql +7 -7
- package/model/graphs/config/fields.graphql +379 -367
- package/model/graphs/config/menu.graphql +54 -54
- package/model/graphs/config/mutation.graphql +30 -30
- package/model/graphs/config/page.graphql +256 -256
- package/model/graphs/config/panel.graphql +30 -30
- package/model/graphs/config/query.graphql +84 -78
- package/model/graphs/config/rules.graphql +80 -0
- package/model/graphs/config/selectOptions.graphql +47 -47
- package/model/graphs/config/table.graphql +182 -182
- package/model/graphs/config/tests.graphql +16 -16
- package/model/graphs/config/theme.graphql +41 -41
- package/model/graphs/config/ui-components.graphql +10 -10
- package/model/graphs/domains/scalars.graphql +3 -3
- package/model/graphs/filter.graphql +5 -5
- package/model/graphs/schema.graphql +7 -7
- package/package.json +79 -81
package/README.md
CHANGED
|
@@ -1,182 +1,182 @@
|
|
|
1
|
-
# @docupace/ihub-config
|
|
2
|
-
|
|
3
|
-
Reusable GraphQL config extension and standalone server extracted from the
|
|
4
|
-
Docupace GraphQL server.
|
|
5
|
-
|
|
6
|
-
## Run locally
|
|
7
|
-
|
|
8
|
-
```bash
|
|
9
|
-
pnpm install
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
To run the package as a standalone GraphQL server, copy
|
|
13
|
-
`.env.example` to `.env`, fill in the required values, and start the dev
|
|
14
|
-
server:
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
cp .env.example .env
|
|
18
|
-
pnpm start:dev
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
The standalone server listens on `http://localhost:4000/api/graphql`.
|
|
22
|
-
|
|
23
|
-
## Use as an extension in another server
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
import { getConfigExtension } from '@docupace/ihub-config';
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Then add the returned extension to the host server's extensions array:
|
|
30
|
-
|
|
31
|
-
```ts
|
|
32
|
-
const configExtension = await getConfigExtension();
|
|
33
|
-
|
|
34
|
-
const serverInfo: ServerConfig = {
|
|
35
|
-
instanceConfig: instanceConfig,
|
|
36
|
-
extensions: [docupaceExtension, configExtension],
|
|
37
|
-
port: 4000,
|
|
38
|
-
createContext,
|
|
39
|
-
};
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Extend the config extension with domain-specific providers, resolvers, etc.
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
import { getConfigExtension } from '@docupace/ihub-config';
|
|
46
|
-
import {
|
|
47
|
-
DashboardPageConfigProvider,
|
|
48
|
-
TablePageConfigProvider,
|
|
49
|
-
AnyPageConfigProvider,
|
|
50
|
-
DetailsPageConfigProvider,
|
|
51
|
-
} from './your-host-config-providers.js';
|
|
52
|
-
import { configResolvers } from './your-host-config-resolvers.js';
|
|
53
|
-
|
|
54
|
-
const APPLICATION_PREFERENCE_PREFIX = 'applicationPreference';
|
|
55
|
-
|
|
56
|
-
const applicationPreferenceResolver = async (keys, context) => {
|
|
57
|
-
const data = await loadApplicationPreferences(keys, context);
|
|
58
|
-
|
|
59
|
-
const result: Record<string, string | undefined> = {};
|
|
60
|
-
keys.forEach((key) => (result[key] = undefined));
|
|
61
|
-
data.records.forEach((record: { name: string; value: string }) => {
|
|
62
|
-
result[record.name] = record.value;
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
return result;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const configExtension = await getConfigExtension({
|
|
69
|
-
configInjectorResolvers: {
|
|
70
|
-
[APPLICATION_PREFERENCE_PREFIX]: applicationPreferenceResolver,
|
|
71
|
-
},
|
|
72
|
-
providers: {
|
|
73
|
-
DashboardPage: DashboardPageConfigProvider,
|
|
74
|
-
TablePage: TablePageConfigProvider,
|
|
75
|
-
AnyPage: AnyPageConfigProvider,
|
|
76
|
-
DetailsPage: DetailsPageConfigProvider,
|
|
77
|
-
},
|
|
78
|
-
resolvers: configResolvers,
|
|
79
|
-
});
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### `configInjectorResolvers`
|
|
83
|
-
|
|
84
|
-
Use `configInjectorResolvers` to resolve template expressions embedded in config
|
|
85
|
-
values, for example:
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
$${applicationPreference:timezone}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
Each key in `configInjectorResolvers` is a template prefix, and each value is an
|
|
92
|
-
async resolver function with this shape:
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
type ResolverFunction = (
|
|
96
|
-
keys: string[],
|
|
97
|
-
context: any,
|
|
98
|
-
) => Promise<Record<string, any>>;
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
When config processing encounters `$${prefix:key}`, it looks up the resolver for
|
|
102
|
-
that `prefix`, batches the requested keys, and replaces the template with the
|
|
103
|
-
resolved value. This is the right place to connect host-specific data sources
|
|
104
|
-
such as application preferences, tenant settings, or user-scoped values.
|
|
105
|
-
|
|
106
|
-
### `providers`
|
|
107
|
-
|
|
108
|
-
Use `providers` to register config processors by `__typename`. A provider runs
|
|
109
|
-
after nested config values have been loaded and template expressions have been
|
|
110
|
-
resolved, and can reshape or enrich the final config object before it is
|
|
111
|
-
returned.
|
|
112
|
-
|
|
113
|
-
```ts
|
|
114
|
-
providers: {
|
|
115
|
-
DashboardPage: DashboardPageConfigProvider,
|
|
116
|
-
TablePage: TablePageConfigProvider,
|
|
117
|
-
AnyPage: AnyPageConfigProvider,
|
|
118
|
-
DetailsPage: DetailsPageConfigProvider,
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Each provider must be a class with a `processConfig` method:
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
interface ConfigProvider {
|
|
126
|
-
processConfig(config: any, configService: ConfigService, context: any): any;
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
This is useful for page-specific post-processing such as normalizing fragments,
|
|
131
|
-
rewriting references, or deriving extra fields for a particular page type.
|
|
132
|
-
Built-in providers are preserved and host providers are merged in on top.
|
|
133
|
-
|
|
134
|
-
### `resolvers`
|
|
135
|
-
|
|
136
|
-
Use `resolvers` to extend the GraphQL API exposed by the config extension.
|
|
137
|
-
Resolvers are provided as factories so they can receive the initialized
|
|
138
|
-
`configService` instance:
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
const configResolvers = {
|
|
142
|
-
query: ({ configService }) => ({
|
|
143
|
-
pages: () => ({
|
|
144
|
-
myCustomPage: async (obj: { path: string }, context: any) => {
|
|
145
|
-
return await configService.getDirectConfig(obj.path, context);
|
|
146
|
-
},
|
|
147
|
-
}),
|
|
148
|
-
}),
|
|
149
|
-
mutation: ({ configService }) => ({
|
|
150
|
-
pages: () => ({
|
|
151
|
-
refreshCustomConfig: async () => {
|
|
152
|
-
await configService.init();
|
|
153
|
-
return true;
|
|
154
|
-
},
|
|
155
|
-
}),
|
|
156
|
-
}),
|
|
157
|
-
};
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
Host query and mutation resolvers are deep-merged with the built-in config
|
|
161
|
-
resolvers, so you can add new fields without re-declaring the entire resolver
|
|
162
|
-
tree. If the same field exists in both places, the host resolver wins.
|
|
163
|
-
|
|
164
|
-
If you want to pass config stores programmatically instead of through
|
|
165
|
-
environment variables, `getConfigExtension` also accepts `configStores` (usually used for tests only).
|
|
166
|
-
|
|
167
|
-
## Environment variables
|
|
168
|
-
|
|
169
|
-
Config stores are discovered from environment variables grouped by prefix such
|
|
170
|
-
as `STANDARD` or `SITE`:
|
|
171
|
-
|
|
172
|
-
- `<PREFIX>_CONFIG_LOCAL_PATH`
|
|
173
|
-
- `<PREFIX>_CONFIG_REMOTE_PATH`
|
|
174
|
-
- `<PREFIX>_CONFIG_REMOTE_BRANCH`
|
|
175
|
-
- `<PREFIX>_CONFIG_PRIORITY`
|
|
176
|
-
- `<PREFIX>_CONFIG_NAME`
|
|
177
|
-
|
|
178
|
-
`<PREFIX>_CONFIG_PRIORITY` is required for every configured store.
|
|
179
|
-
|
|
180
|
-
`CONFIG_LOCAL_PATH_ROOT` can be used as a fallback base path when
|
|
181
|
-
`<PREFIX>_CONFIG_LOCAL_PATH` is omitted, but the store still needs enough
|
|
182
|
-
configuration to be discovered.
|
|
1
|
+
# @docupace/ihub-config
|
|
2
|
+
|
|
3
|
+
Reusable GraphQL config extension and standalone server extracted from the
|
|
4
|
+
Docupace GraphQL server.
|
|
5
|
+
|
|
6
|
+
## Run locally
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm install
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
To run the package as a standalone GraphQL server, copy
|
|
13
|
+
`.env.example` to `.env`, fill in the required values, and start the dev
|
|
14
|
+
server:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cp .env.example .env
|
|
18
|
+
pnpm start:dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The standalone server listens on `http://localhost:4000/api/graphql`.
|
|
22
|
+
|
|
23
|
+
## Use as an extension in another server
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { getConfigExtension } from '@docupace/ihub-config';
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then add the returned extension to the host server's extensions array:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const configExtension = await getConfigExtension();
|
|
33
|
+
|
|
34
|
+
const serverInfo: ServerConfig = {
|
|
35
|
+
instanceConfig: instanceConfig,
|
|
36
|
+
extensions: [docupaceExtension, configExtension],
|
|
37
|
+
port: 4000,
|
|
38
|
+
createContext,
|
|
39
|
+
};
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Extend the config extension with domain-specific providers, resolvers, etc.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { getConfigExtension } from '@docupace/ihub-config';
|
|
46
|
+
import {
|
|
47
|
+
DashboardPageConfigProvider,
|
|
48
|
+
TablePageConfigProvider,
|
|
49
|
+
AnyPageConfigProvider,
|
|
50
|
+
DetailsPageConfigProvider,
|
|
51
|
+
} from './your-host-config-providers.js';
|
|
52
|
+
import { configResolvers } from './your-host-config-resolvers.js';
|
|
53
|
+
|
|
54
|
+
const APPLICATION_PREFERENCE_PREFIX = 'applicationPreference';
|
|
55
|
+
|
|
56
|
+
const applicationPreferenceResolver = async (keys, context) => {
|
|
57
|
+
const data = await loadApplicationPreferences(keys, context);
|
|
58
|
+
|
|
59
|
+
const result: Record<string, string | undefined> = {};
|
|
60
|
+
keys.forEach((key) => (result[key] = undefined));
|
|
61
|
+
data.records.forEach((record: { name: string; value: string }) => {
|
|
62
|
+
result[record.name] = record.value;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const configExtension = await getConfigExtension({
|
|
69
|
+
configInjectorResolvers: {
|
|
70
|
+
[APPLICATION_PREFERENCE_PREFIX]: applicationPreferenceResolver,
|
|
71
|
+
},
|
|
72
|
+
providers: {
|
|
73
|
+
DashboardPage: DashboardPageConfigProvider,
|
|
74
|
+
TablePage: TablePageConfigProvider,
|
|
75
|
+
AnyPage: AnyPageConfigProvider,
|
|
76
|
+
DetailsPage: DetailsPageConfigProvider,
|
|
77
|
+
},
|
|
78
|
+
resolvers: configResolvers,
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `configInjectorResolvers`
|
|
83
|
+
|
|
84
|
+
Use `configInjectorResolvers` to resolve template expressions embedded in config
|
|
85
|
+
values, for example:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
$${applicationPreference:timezone}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Each key in `configInjectorResolvers` is a template prefix, and each value is an
|
|
92
|
+
async resolver function with this shape:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
type ResolverFunction = (
|
|
96
|
+
keys: string[],
|
|
97
|
+
context: any,
|
|
98
|
+
) => Promise<Record<string, any>>;
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
When config processing encounters `$${prefix:key}`, it looks up the resolver for
|
|
102
|
+
that `prefix`, batches the requested keys, and replaces the template with the
|
|
103
|
+
resolved value. This is the right place to connect host-specific data sources
|
|
104
|
+
such as application preferences, tenant settings, or user-scoped values.
|
|
105
|
+
|
|
106
|
+
### `providers`
|
|
107
|
+
|
|
108
|
+
Use `providers` to register config processors by `__typename`. A provider runs
|
|
109
|
+
after nested config values have been loaded and template expressions have been
|
|
110
|
+
resolved, and can reshape or enrich the final config object before it is
|
|
111
|
+
returned.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
providers: {
|
|
115
|
+
DashboardPage: DashboardPageConfigProvider,
|
|
116
|
+
TablePage: TablePageConfigProvider,
|
|
117
|
+
AnyPage: AnyPageConfigProvider,
|
|
118
|
+
DetailsPage: DetailsPageConfigProvider,
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Each provider must be a class with a `processConfig` method:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
interface ConfigProvider {
|
|
126
|
+
processConfig(config: any, configService: ConfigService, context: any): any;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This is useful for page-specific post-processing such as normalizing fragments,
|
|
131
|
+
rewriting references, or deriving extra fields for a particular page type.
|
|
132
|
+
Built-in providers are preserved and host providers are merged in on top.
|
|
133
|
+
|
|
134
|
+
### `resolvers`
|
|
135
|
+
|
|
136
|
+
Use `resolvers` to extend the GraphQL API exposed by the config extension.
|
|
137
|
+
Resolvers are provided as factories so they can receive the initialized
|
|
138
|
+
`configService` instance:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const configResolvers = {
|
|
142
|
+
query: ({ configService }) => ({
|
|
143
|
+
pages: () => ({
|
|
144
|
+
myCustomPage: async (obj: { path: string }, context: any) => {
|
|
145
|
+
return await configService.getDirectConfig(obj.path, context);
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
}),
|
|
149
|
+
mutation: ({ configService }) => ({
|
|
150
|
+
pages: () => ({
|
|
151
|
+
refreshCustomConfig: async () => {
|
|
152
|
+
await configService.init();
|
|
153
|
+
return true;
|
|
154
|
+
},
|
|
155
|
+
}),
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Host query and mutation resolvers are deep-merged with the built-in config
|
|
161
|
+
resolvers, so you can add new fields without re-declaring the entire resolver
|
|
162
|
+
tree. If the same field exists in both places, the host resolver wins.
|
|
163
|
+
|
|
164
|
+
If you want to pass config stores programmatically instead of through
|
|
165
|
+
environment variables, `getConfigExtension` also accepts `configStores` (usually used for tests only).
|
|
166
|
+
|
|
167
|
+
## Environment variables
|
|
168
|
+
|
|
169
|
+
Config stores are discovered from environment variables grouped by prefix such
|
|
170
|
+
as `STANDARD` or `SITE`:
|
|
171
|
+
|
|
172
|
+
- `<PREFIX>_CONFIG_LOCAL_PATH`
|
|
173
|
+
- `<PREFIX>_CONFIG_REMOTE_PATH`
|
|
174
|
+
- `<PREFIX>_CONFIG_REMOTE_BRANCH`
|
|
175
|
+
- `<PREFIX>_CONFIG_PRIORITY`
|
|
176
|
+
- `<PREFIX>_CONFIG_NAME`
|
|
177
|
+
|
|
178
|
+
`<PREFIX>_CONFIG_PRIORITY` is required for every configured store.
|
|
179
|
+
|
|
180
|
+
`CONFIG_LOCAL_PATH_ROOT` can be used as a fallback base path when
|
|
181
|
+
`<PREFIX>_CONFIG_LOCAL_PATH` is omitted, but the store still needs enough
|
|
182
|
+
configuration to be discovered.
|
package/dist/config-service.d.ts
CHANGED
|
@@ -75,7 +75,15 @@ declare class ConfigService {
|
|
|
75
75
|
getDirectConfig(configPath: string, context: any, values?: any, processProviders?: boolean): Promise<any>;
|
|
76
76
|
listDirectConfigs(configPath: string): Promise<any[]>;
|
|
77
77
|
updateConfig(configPath: string, config: any, context: any): Promise<any>;
|
|
78
|
-
|
|
78
|
+
private normalizePathForMatching;
|
|
79
|
+
private applyFieldRuleToElement;
|
|
80
|
+
private applyOverridesToConfig;
|
|
81
|
+
private resolveQuerySelectOptions;
|
|
82
|
+
private inlineQuerySelectOptions;
|
|
83
|
+
private stripNulls;
|
|
84
|
+
private applyFieldRuleOverrides;
|
|
85
|
+
getRuleGroups(rulesContext: any, context: any): Promise<any[]>;
|
|
86
|
+
getPageConfig(pagePath: string, context: any, processConfig?: boolean, rulesContext?: any): Promise<any>;
|
|
79
87
|
init(): Promise<void[]>;
|
|
80
88
|
}
|
|
81
89
|
export declare const defaultConfigService: ({ model, configStores, configInjectorResolvers, providers, resolvers, }: DefaultConfigServiceParams) => ConfigService;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-service.d.ts","sourceRoot":"","sources":["../src/config-service.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"config-service.d.ts","sourceRoot":"","sources":["../src/config-service.ts"],"names":[],"mappings":"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"}
|
package/dist/config-service.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { promises as fsPromises } from 'node:fs';
|
|
3
3
|
import { simpleGit } from 'simple-git';
|
|
4
|
-
import { _logger as log } from './globals.js';
|
|
4
|
+
import { _logger as log, cliOptions } from './globals.js';
|
|
5
5
|
import { GraphQLError } from 'graphql';
|
|
6
6
|
import { fileExists } from './lib/file-system.js';
|
|
7
7
|
import replaceDotsOutsideExpressions from './lib/replace-dots-outside-expressions.js';
|
|
@@ -10,6 +10,7 @@ import { injectConfigValue, } from './injector-resolvers/config-injector.js';
|
|
|
10
10
|
import { builtInProviders } from './providers/index.js';
|
|
11
11
|
import { deepMergeResolvers, } from './resolvers/index.js';
|
|
12
12
|
import { dump, load } from 'js-yaml';
|
|
13
|
+
import { buildQuery } from '@docupace/ihub-core/query';
|
|
13
14
|
const FULL_OBJECT_EXPRESSION_PATTERN = /^\${(.*?)}$/;
|
|
14
15
|
const INLINE_OBJECT_EXPRESSION_PATTERN = /\${(.*?)}/g;
|
|
15
16
|
export class ConfigFileStore {
|
|
@@ -261,9 +262,19 @@ class ConfigService {
|
|
|
261
262
|
const builtInQueryResolvers = {
|
|
262
263
|
pages: () => ({
|
|
263
264
|
getPageConfig: async (obj, context) => {
|
|
264
|
-
const pageConfig = await this.getPageConfig(obj.path, context, true);
|
|
265
|
+
const pageConfig = await this.getPageConfig(obj.path, context, true, obj.context);
|
|
265
266
|
return pageConfig;
|
|
266
267
|
},
|
|
268
|
+
getAIConfig: async (obj, context) => {
|
|
269
|
+
const pageConfig = await this.getPageConfig(obj.path, context, true, obj.context);
|
|
270
|
+
const inlined = await this.inlineQuerySelectOptions(pageConfig, context);
|
|
271
|
+
return this.stripNulls(inlined) ?? null;
|
|
272
|
+
},
|
|
273
|
+
}),
|
|
274
|
+
rules: () => ({
|
|
275
|
+
getRuleGroups: async (obj, context) => {
|
|
276
|
+
return await this.getRuleGroups(obj.context, context);
|
|
277
|
+
},
|
|
267
278
|
}),
|
|
268
279
|
widgets: () => ({
|
|
269
280
|
list: async (obj, context) => {
|
|
@@ -545,10 +556,193 @@ class ConfigService {
|
|
|
545
556
|
await this.configStores[0].updateConfig(globalPath, config);
|
|
546
557
|
return await this.getConfig(configPath, context, null, true);
|
|
547
558
|
}
|
|
548
|
-
|
|
549
|
-
|
|
559
|
+
normalizePathForMatching(fieldPath) {
|
|
560
|
+
return fieldPath
|
|
561
|
+
.split('.')
|
|
562
|
+
.filter((segment) => !segment.startsWith('@'))
|
|
563
|
+
.join('.');
|
|
564
|
+
}
|
|
565
|
+
applyFieldRuleToElement(element, fieldRule) {
|
|
566
|
+
const result = { ...element };
|
|
567
|
+
const applyIfUndefined = (fieldKey, ruleKey = fieldKey) => {
|
|
568
|
+
if ((result[fieldKey] === undefined || result[fieldKey] === null) &&
|
|
569
|
+
fieldRule[ruleKey] !== undefined) {
|
|
570
|
+
result[fieldKey] = fieldRule[ruleKey];
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
applyIfUndefined('required');
|
|
574
|
+
applyIfUndefined('readonly');
|
|
575
|
+
applyIfUndefined('defaultValue');
|
|
576
|
+
applyIfUndefined('messageLabel');
|
|
577
|
+
applyIfUndefined('requiredCondition');
|
|
578
|
+
applyIfUndefined('visibilityCondition', 'availabilityCondition');
|
|
579
|
+
applyIfUndefined('maxLength');
|
|
580
|
+
applyIfUndefined('minLength');
|
|
581
|
+
applyIfUndefined('validation');
|
|
582
|
+
applyIfUndefined('minDate');
|
|
583
|
+
applyIfUndefined('maxDate');
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
applyOverridesToConfig(config, fieldRuleMap) {
|
|
587
|
+
if (!config || typeof config !== 'object') {
|
|
588
|
+
return config;
|
|
589
|
+
}
|
|
590
|
+
if (Array.isArray(config)) {
|
|
591
|
+
return config.map((item) => this.applyOverridesToConfig(item, fieldRuleMap));
|
|
592
|
+
}
|
|
593
|
+
let result = { ...config };
|
|
594
|
+
if (typeof result.path === 'string') {
|
|
595
|
+
const normalizedPath = this.normalizePathForMatching(result.path);
|
|
596
|
+
const fieldRule = fieldRuleMap.get(normalizedPath);
|
|
597
|
+
if (fieldRule) {
|
|
598
|
+
result = this.applyFieldRuleToElement(result, fieldRule);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
for (const key of Object.keys(result)) {
|
|
602
|
+
if (result[key] && typeof result[key] === 'object') {
|
|
603
|
+
result[key] = this.applyOverridesToConfig(result[key], fieldRuleMap);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return result;
|
|
607
|
+
}
|
|
608
|
+
async resolveQuerySelectOptions(selectOptions, context) {
|
|
609
|
+
// Skip dynamic queries that require runtime form state or search-as-you-type
|
|
610
|
+
if (!this.model ||
|
|
611
|
+
selectOptions.queryOnTyping ||
|
|
612
|
+
selectOptions.variablesExpressions) {
|
|
613
|
+
return selectOptions;
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
// When apiLevel is '2', list endpoints wrap results in { items: [...] }.
|
|
617
|
+
// buildQueryStructFromInput does not check cliOptions itself, so we must
|
|
618
|
+
// pass pathPrefix explicitly to get `items { fields }` in the query.
|
|
619
|
+
const effectivePathPrefix = selectOptions.responsePathPrefix ??
|
|
620
|
+
(cliOptions.apiLevel === '2' ? 'items' : undefined);
|
|
621
|
+
const query = buildQuery({
|
|
622
|
+
queryName: selectOptions.queryName,
|
|
623
|
+
operationName: selectOptions.operationName || 'AIConfigQuery',
|
|
624
|
+
paths: selectOptions.paths || [],
|
|
625
|
+
variableDefinitions: selectOptions.variableDefinitions,
|
|
626
|
+
pathPrefix: effectivePathPrefix,
|
|
627
|
+
});
|
|
628
|
+
const result = await this.model.executeOperation({ query, variables: selectOptions.variables ?? {} }, context);
|
|
629
|
+
if (result.errors?.length > 0) {
|
|
630
|
+
log.warn({ errors: result.errors }, 'QuerySelectOptions resolution failed');
|
|
631
|
+
return selectOptions;
|
|
632
|
+
}
|
|
633
|
+
const navPath = effectivePathPrefix
|
|
634
|
+
? `${selectOptions.queryName}.${effectivePathPrefix}`
|
|
635
|
+
: selectOptions.queryName;
|
|
636
|
+
let data = result.data;
|
|
637
|
+
for (const part of navPath.split('.')) {
|
|
638
|
+
data = data?.[part];
|
|
639
|
+
}
|
|
640
|
+
if (Array.isArray(data)) {
|
|
641
|
+
return data;
|
|
642
|
+
}
|
|
643
|
+
return data == null ? [] : [data];
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
log.warn({ error }, 'Failed to resolve QuerySelectOptions');
|
|
647
|
+
return selectOptions;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
async inlineQuerySelectOptions(config, context) {
|
|
651
|
+
if (config === null || config === undefined) {
|
|
652
|
+
return config;
|
|
653
|
+
}
|
|
654
|
+
if (Array.isArray(config)) {
|
|
655
|
+
return Promise.all(config.map((item) => this.inlineQuerySelectOptions(item, context)));
|
|
656
|
+
}
|
|
657
|
+
if (typeof config === 'object') {
|
|
658
|
+
if (config.__typename === 'QuerySelectOptions') {
|
|
659
|
+
return this.resolveQuerySelectOptions(config, context);
|
|
660
|
+
}
|
|
661
|
+
const entries = Object.entries(config);
|
|
662
|
+
const processedValues = await Promise.all(entries.map(([, value]) => this.inlineQuerySelectOptions(value, context)));
|
|
663
|
+
return Object.fromEntries(entries.map(([key], index) => [key, processedValues[index]]));
|
|
664
|
+
}
|
|
550
665
|
return config;
|
|
551
666
|
}
|
|
667
|
+
stripNulls(obj) {
|
|
668
|
+
if (obj === null || obj === undefined) {
|
|
669
|
+
return undefined;
|
|
670
|
+
}
|
|
671
|
+
if (Array.isArray(obj)) {
|
|
672
|
+
return obj
|
|
673
|
+
.map((item) => this.stripNulls(item))
|
|
674
|
+
.filter((item) => item !== undefined);
|
|
675
|
+
}
|
|
676
|
+
if (typeof obj === 'object') {
|
|
677
|
+
const result = {};
|
|
678
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
679
|
+
const stripped = this.stripNulls(value);
|
|
680
|
+
if (stripped !== undefined) {
|
|
681
|
+
result[key] = stripped;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return result;
|
|
685
|
+
}
|
|
686
|
+
return obj;
|
|
687
|
+
}
|
|
688
|
+
applyFieldRuleOverrides(config, matchingRuleGroups) {
|
|
689
|
+
// Sort ascending by priority so higher priority value overwrites lower
|
|
690
|
+
const sorted = [...matchingRuleGroups].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
691
|
+
const fieldRuleMap = new Map();
|
|
692
|
+
for (const ruleGroup of sorted) {
|
|
693
|
+
for (const fieldRule of ruleGroup.fieldRules ?? []) {
|
|
694
|
+
if (!fieldRule.path) {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
const normalizedPath = this.normalizePathForMatching(fieldRule.path);
|
|
698
|
+
const existing = fieldRuleMap.get(normalizedPath);
|
|
699
|
+
fieldRuleMap.set(normalizedPath, existing ? { ...existing, ...fieldRule } : { ...fieldRule });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (fieldRuleMap.size === 0) {
|
|
703
|
+
return config;
|
|
704
|
+
}
|
|
705
|
+
return this.applyOverridesToConfig(config, fieldRuleMap);
|
|
706
|
+
}
|
|
707
|
+
async getRuleGroups(rulesContext, context) {
|
|
708
|
+
const ruleFiles = await this.listDirectConfigs('global/rules');
|
|
709
|
+
const allRuleGroups = [];
|
|
710
|
+
for (const ruleFile of ruleFiles) {
|
|
711
|
+
try {
|
|
712
|
+
const ruleGroups = await this.getDirectConfig(path.join('global', 'rules', ruleFile), context, null, false);
|
|
713
|
+
if (Array.isArray(ruleGroups)) {
|
|
714
|
+
allRuleGroups.push(...ruleGroups);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
// skip unloadable rule files
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return allRuleGroups.filter((ruleGroup) => {
|
|
722
|
+
if (!ruleGroup.condition) {
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
if (!rulesContext) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
try {
|
|
729
|
+
const keys = Object.keys(rulesContext);
|
|
730
|
+
const values = Object.values(rulesContext);
|
|
731
|
+
return new Function(...keys, `return ${ruleGroup.condition};`)(...values);
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
async getPageConfig(pagePath, context, processConfig, rulesContext) {
|
|
739
|
+
const globalPath = path.join('global', 'pages', pagePath);
|
|
740
|
+
const obj = await this.getConfigFromStore(globalPath);
|
|
741
|
+
const fullObj = await this.processObject(globalPath, obj, context, null);
|
|
742
|
+
const matchingRuleGroups = await this.getRuleGroups(rulesContext, context);
|
|
743
|
+
const configToProcess = this.applyFieldRuleOverrides(fullObj, matchingRuleGroups);
|
|
744
|
+
return await this.processConfig(configToProcess, context, processConfig);
|
|
745
|
+
}
|
|
552
746
|
async init() {
|
|
553
747
|
return Promise.all(this.configStores.map((configStore) => configStore.init()));
|
|
554
748
|
}
|