@config-bound/schema-export 0.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.
@@ -0,0 +1,161 @@
1
+ import { formatAsJSON, formatAsYAML, formatAsEnvExample } from './schemaFormatters.js';
2
+ describe('Schema Formatters', () => {
3
+ const mockSchema = {
4
+ name: 'testApp',
5
+ sections: [
6
+ {
7
+ name: 'app',
8
+ description: 'Application settings',
9
+ elements: [
10
+ {
11
+ name: 'port',
12
+ description: 'Server port',
13
+ type: 'number',
14
+ default: 3000,
15
+ example: 8080,
16
+ required: false,
17
+ sensitive: false,
18
+ joiValidation: {
19
+ type: 'number',
20
+ rules: [
21
+ { name: 'min', args: { limit: 1 } },
22
+ { name: 'max', args: { limit: 65535 } }
23
+ ]
24
+ }
25
+ },
26
+ {
27
+ name: 'apiKey',
28
+ description: 'API key',
29
+ type: 'string',
30
+ default: undefined,
31
+ example: 'sk_test_123',
32
+ required: true,
33
+ sensitive: true,
34
+ joiValidation: { type: 'string', flags: { presence: 'required' } }
35
+ }
36
+ ]
37
+ },
38
+ {
39
+ name: 'database',
40
+ description: 'Database configuration',
41
+ elements: [
42
+ {
43
+ name: 'host',
44
+ description: 'Database host',
45
+ type: 'string',
46
+ default: 'localhost',
47
+ example: undefined,
48
+ required: false,
49
+ sensitive: false,
50
+ joiValidation: { type: 'string' }
51
+ }
52
+ ]
53
+ }
54
+ ]
55
+ };
56
+ describe('formatAsJSON', () => {
57
+ it('should format schema as pretty JSON', () => {
58
+ const result = formatAsJSON(mockSchema, true);
59
+ expect(result).toContain('"name": "testApp"');
60
+ expect(result).toContain('"port"');
61
+ expect(result).toContain('"database"');
62
+ expect(JSON.parse(result)).toEqual(mockSchema);
63
+ });
64
+ it('should format schema as compact JSON', () => {
65
+ const result = formatAsJSON(mockSchema, false);
66
+ expect(result).not.toContain('\n');
67
+ expect(JSON.parse(result)).toEqual(mockSchema);
68
+ });
69
+ });
70
+ describe('formatAsYAML', () => {
71
+ it('should format schema as YAML', () => {
72
+ const result = formatAsYAML(mockSchema);
73
+ expect(result).toContain('name: testApp');
74
+ expect(result).toContain('- name: port');
75
+ expect(result).toContain('- name: database');
76
+ expect(result).toBeTruthy();
77
+ });
78
+ });
79
+ describe('formatAsEnvExample', () => {
80
+ it('should format schema as .env.example without prefix', () => {
81
+ const result = formatAsEnvExample(mockSchema);
82
+ expect(result).toContain('# testApp Configuration');
83
+ expect(result).toContain('# Generated from schema export');
84
+ expect(result).toContain('APP_PORT=');
85
+ expect(result).toContain('APP_APIKEY=');
86
+ expect(result).toContain('DATABASE_HOST=');
87
+ expect(result).toContain('# Server port');
88
+ expect(result).toContain('# API key');
89
+ expect(result).toContain('# Database host');
90
+ });
91
+ it('should format schema as .env.example with prefix', () => {
92
+ const result = formatAsEnvExample(mockSchema, 'MYAPP');
93
+ expect(result).toContain('MYAPP_APP_PORT=');
94
+ expect(result).toContain('MYAPP_APP_APIKEY=');
95
+ expect(result).toContain('MYAPP_DATABASE_HOST=');
96
+ });
97
+ it('should include example values when available', () => {
98
+ const result = formatAsEnvExample(mockSchema);
99
+ expect(result).toContain('APP_PORT=8080');
100
+ });
101
+ it('should include default values when example is not available', () => {
102
+ const result = formatAsEnvExample(mockSchema);
103
+ expect(result).toContain('DATABASE_HOST=localhost');
104
+ });
105
+ it('should use placeholder for sensitive values', () => {
106
+ const result = formatAsEnvExample(mockSchema);
107
+ expect(result).toContain('APP_APIKEY=your-example-value');
108
+ });
109
+ it('should mark required fields', () => {
110
+ const result = formatAsEnvExample(mockSchema);
111
+ expect(result).toContain('# API key (required)');
112
+ });
113
+ it('should handle sections without descriptions', () => {
114
+ const schemaWithoutDesc = {
115
+ name: 'test',
116
+ sections: [
117
+ {
118
+ name: 'app',
119
+ elements: [
120
+ {
121
+ name: 'port',
122
+ type: 'number',
123
+ default: 3000,
124
+ required: false,
125
+ sensitive: false,
126
+ joiValidation: {}
127
+ }
128
+ ]
129
+ }
130
+ ]
131
+ };
132
+ const result = formatAsEnvExample(schemaWithoutDesc);
133
+ expect(result).toContain('APP_PORT=3000');
134
+ });
135
+ it('should handle elements without descriptions', () => {
136
+ const schemaWithoutElementDesc = {
137
+ name: 'test',
138
+ sections: [
139
+ {
140
+ name: 'app',
141
+ description: 'App config',
142
+ elements: [
143
+ {
144
+ name: 'port',
145
+ type: 'number',
146
+ default: 3000,
147
+ required: false,
148
+ sensitive: false,
149
+ joiValidation: {}
150
+ }
151
+ ]
152
+ }
153
+ ]
154
+ };
155
+ const result = formatAsEnvExample(schemaWithoutElementDesc);
156
+ expect(result).toContain('# App config');
157
+ expect(result).toContain('APP_PORT=3000');
158
+ });
159
+ });
160
+ });
161
+ //# sourceMappingURL=schemaFormatters.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemaFormatters.spec.js","sourceRoot":"","sources":["../src/schemaFormatters.spec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import configBoundConfig from '@config-bound/eslint-config/package.eslint.mjs';
2
+
3
+ export default configBoundConfig;
package/jest.config.js ADDED
@@ -0,0 +1,26 @@
1
+ /* eslint-disable @typescript-eslint/naming-convention */
2
+
3
+ /** @type {import('ts-jest').JestConfigWithTsJest} **/
4
+ export default {
5
+ testEnvironment: 'node',
6
+ rootDir: './src',
7
+ testMatch: ['**/*.spec.ts'],
8
+ extensionsToTreatAsEsm: ['.ts'],
9
+ moduleNameMapper: {
10
+ '^(\\.{1,2}/.*)\\.js$': '$1'
11
+ },
12
+ transform: {
13
+ '^.+\\.tsx?$': [
14
+ 'ts-jest',
15
+ {
16
+ useESM: true,
17
+ tsconfig: {
18
+ isolatedModules: false
19
+ },
20
+ diagnostics: {
21
+ ignoreCodes: [151002]
22
+ }
23
+ }
24
+ ]
25
+ }
26
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@config-bound/schema-export",
3
+ "version": "0.1.0",
4
+ "description": "Schema export utilities for ConfigBound configuration library",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "require": "./dist/index.js",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "clean": "rimraf dist",
18
+ "test": "jest",
19
+ "lint": "eslint src/**/*.ts --fix",
20
+ "lint:ci": "eslint src/**/*.ts",
21
+ "format": "prettier --write --config ../../.config/.prettierrc --ignore-path ../../.config/.prettierignore src/**/*.ts",
22
+ "format:ci": "prettier --check --config ../../.config/.prettierrc --ignore-path ../../.config/.prettierignore src/**/*.ts"
23
+ },
24
+ "keywords": [
25
+ "config",
26
+ "configuration",
27
+ "schema",
28
+ "export",
29
+ "json",
30
+ "yaml",
31
+ "documentation"
32
+ ],
33
+ "author": "Robert Keyser",
34
+ "license": "MIT",
35
+ "dependencies": {
36
+ "@config-bound/config-bound": "0.1.0",
37
+ "js-yaml": "^4.1.0"
38
+ },
39
+ "devDependencies": {
40
+ "@config-bound/eslint-config": "*",
41
+ "@types/jest": "^30.0.0",
42
+ "@types/js-yaml": "^4.0.9",
43
+ "@types/node": "^24.10.0",
44
+ "eslint": "^9.39.1",
45
+ "jest": "^30.2.0",
46
+ "rimraf": "^6.1.0",
47
+ "ts-jest": "^29.4.5",
48
+ "typescript": "^5.9.3"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public",
52
+ "registry": "https://registry.npmjs.org"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "https://github.com/notr-ai/ConfigBound"
57
+ }
58
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export {
2
+ exportElement,
3
+ exportSection,
4
+ exportSchema,
5
+ type ExportedElement,
6
+ type ExportedSection,
7
+ type ExportedSchema
8
+ } from './schemaExporter.js';
9
+
10
+ export {
11
+ formatAsJSON,
12
+ formatAsYAML,
13
+ formatAsEnvExample
14
+ } from './schemaFormatters.js';
@@ -0,0 +1,286 @@
1
+ import { Section } from '@config-bound/config-bound/section';
2
+ import { Element } from '@config-bound/config-bound/element';
3
+ import Joi from 'joi';
4
+ import {
5
+ exportElement,
6
+ exportSection,
7
+ exportSchema
8
+ } from './schemaExporter.js';
9
+
10
+ describe('Schema Exporter', () => {
11
+ describe('exportElement', () => {
12
+ it('should export a basic element', () => {
13
+ const element = new Element<string>(
14
+ 'testElement',
15
+ 'A test element',
16
+ 'default-value',
17
+ 'example-value',
18
+ false,
19
+ false,
20
+ Joi.string()
21
+ );
22
+
23
+ const exported = exportElement(element);
24
+
25
+ expect(exported.name).toBe('testElement');
26
+ expect(exported.description).toBe('A test element');
27
+ expect(exported.type).toBe('string');
28
+ expect(exported.default).toBe('default-value');
29
+ expect(exported.example).toBe('example-value');
30
+ expect(exported.required).toBe(false);
31
+ expect(exported.sensitive).toBe(false);
32
+ });
33
+
34
+ it('should handle required elements', () => {
35
+ const element = new Element<number>(
36
+ 'requiredElement',
37
+ 'A required element',
38
+ undefined,
39
+ 42,
40
+ false,
41
+ false,
42
+ Joi.number().required()
43
+ );
44
+
45
+ const exported = exportElement(element);
46
+
47
+ expect(exported.required).toBe(true);
48
+ });
49
+
50
+ it('should handle sensitive elements', () => {
51
+ const element = new Element<string>(
52
+ 'secretKey',
53
+ 'API secret key',
54
+ undefined,
55
+ 'sk_test_123',
56
+ true,
57
+ false,
58
+ Joi.string()
59
+ );
60
+
61
+ const exported = exportElement(element);
62
+
63
+ expect(exported.sensitive).toBe(true);
64
+ });
65
+
66
+ it('should handle elements with omitFromSchema', () => {
67
+ const element = new Element<string>(
68
+ 'privateKey',
69
+ 'Private configuration key',
70
+ 'default',
71
+ 'example',
72
+ false,
73
+ true,
74
+ Joi.string()
75
+ );
76
+
77
+ expect(element.omitFromSchema).toBe(true);
78
+ });
79
+
80
+ it('should extract joi validation object', () => {
81
+ const element = new Element<number>(
82
+ 'port',
83
+ 'Server port',
84
+ 3000,
85
+ undefined,
86
+ false,
87
+ false,
88
+ Joi.number().min(1).max(65535).required()
89
+ );
90
+
91
+ const exported = exportElement(element);
92
+
93
+ expect(exported.joiValidation).toBeDefined();
94
+ expect((exported.joiValidation as any).type).toBe('number');
95
+ expect((exported.joiValidation as any).rules).toBeDefined();
96
+ });
97
+
98
+ it('should handle enum types', () => {
99
+ const element = new Element<string>(
100
+ 'environment',
101
+ 'Runtime environment',
102
+ 'development',
103
+ undefined,
104
+ false,
105
+ false,
106
+ Joi.string().valid('development', 'staging', 'production')
107
+ );
108
+
109
+ const exported = exportElement(element);
110
+
111
+ expect(exported.type).toContain('development');
112
+ expect(exported.type).toContain('staging');
113
+ expect(exported.type).toContain('production');
114
+ });
115
+ });
116
+
117
+ describe('exportSection', () => {
118
+ it('should export a section with elements', () => {
119
+ const elements = [
120
+ new Element<string>('host', 'Database host', 'localhost'),
121
+ new Element<number>('port', 'Database port', 5432)
122
+ ];
123
+
124
+ const section = new Section(
125
+ 'database',
126
+ elements,
127
+ 'Database configuration'
128
+ );
129
+
130
+ const exported = exportSection(section);
131
+
132
+ expect(exported.name).toBe('database');
133
+ expect(exported.description).toBe('Database configuration');
134
+ expect(exported.elements).toHaveLength(2);
135
+ expect(exported.elements[0].name).toBe('host');
136
+ expect(exported.elements[1].name).toBe('port');
137
+ });
138
+
139
+ it('should handle empty sections', () => {
140
+ const section = new Section('empty', [], 'Empty section');
141
+
142
+ const exported = exportSection(section);
143
+
144
+ expect(exported.name).toBe('empty');
145
+ expect(exported.elements).toHaveLength(0);
146
+ });
147
+
148
+ it('should filter out elements with omitFromSchema: true', () => {
149
+ const publicElement = new Element<string>(
150
+ 'appName',
151
+ 'Application name',
152
+ 'myApp',
153
+ 'exampleApp',
154
+ false,
155
+ false,
156
+ Joi.string()
157
+ );
158
+ const privateElement = new Element<string>(
159
+ 'internalKey',
160
+ 'Internal configuration key',
161
+ 'secret',
162
+ 'example',
163
+ false,
164
+ true,
165
+ Joi.string()
166
+ );
167
+
168
+ const section = new Section(
169
+ 'app',
170
+ [publicElement, privateElement],
171
+ 'Application settings'
172
+ );
173
+ const exported = exportSection(section);
174
+
175
+ expect(exported.name).toBe('app');
176
+ expect(exported.description).toBe('Application settings');
177
+ expect(exported.elements).toHaveLength(1);
178
+ expect(exported.elements[0].name).toBe('appName');
179
+ expect(
180
+ exported.elements.find(
181
+ (e: { name: string }) => e.name === 'internalKey'
182
+ )
183
+ ).toBeUndefined();
184
+ });
185
+
186
+ it('should include omitted elements when includeOmitted is true', () => {
187
+ const publicElement = new Element<string>(
188
+ 'appName',
189
+ 'Application name',
190
+ 'myApp',
191
+ 'exampleApp',
192
+ false,
193
+ false,
194
+ Joi.string()
195
+ );
196
+ const privateElement = new Element<string>(
197
+ 'internalKey',
198
+ 'Internal configuration key',
199
+ 'secret',
200
+ 'example',
201
+ false,
202
+ true,
203
+ Joi.string()
204
+ );
205
+
206
+ const section = new Section(
207
+ 'app',
208
+ [publicElement, privateElement],
209
+ 'Application settings'
210
+ );
211
+ const exported = exportSection(section, true);
212
+
213
+ expect(exported.name).toBe('app');
214
+ expect(exported.description).toBe('Application settings');
215
+ expect(exported.elements).toHaveLength(2);
216
+ expect(exported.elements[0].name).toBe('appName');
217
+ expect(exported.elements[1].name).toBe('internalKey');
218
+ });
219
+ });
220
+
221
+ describe('exportSchema', () => {
222
+ it('should export a complete schema', () => {
223
+ const appElements = [
224
+ new Element<number>('port', 'Application port', 3000),
225
+ new Element<string>('host', 'Application host', 'localhost')
226
+ ];
227
+
228
+ const dbElements = [
229
+ new Element<string>(
230
+ 'connectionString',
231
+ 'Database connection',
232
+ undefined
233
+ )
234
+ ];
235
+
236
+ const sections = [
237
+ new Section('app', appElements, 'Application settings'),
238
+ new Section('database', dbElements, 'Database settings')
239
+ ];
240
+
241
+ const schema = exportSchema('myApp', sections);
242
+
243
+ expect(schema.name).toBe('myApp');
244
+ expect(schema.sections).toHaveLength(2);
245
+ expect(schema.sections[0].name).toBe('app');
246
+ expect(schema.sections[1].name).toBe('database');
247
+ });
248
+
249
+ it('should include omitted elements when includeOmitted is true', () => {
250
+ const publicElement = new Element<string>(
251
+ 'appName',
252
+ 'Application name',
253
+ 'myApp',
254
+ 'exampleApp',
255
+ false,
256
+ false,
257
+ Joi.string()
258
+ );
259
+ const privateElement = new Element<string>(
260
+ 'internalKey',
261
+ 'Internal configuration key',
262
+ 'secret',
263
+ 'example',
264
+ false,
265
+ true,
266
+ Joi.string()
267
+ );
268
+
269
+ const sections = [
270
+ new Section(
271
+ 'app',
272
+ [publicElement, privateElement],
273
+ 'Application settings'
274
+ )
275
+ ];
276
+
277
+ const schema = exportSchema('myApp', sections, true);
278
+
279
+ expect(schema.name).toBe('myApp');
280
+ expect(schema.sections).toHaveLength(1);
281
+ expect(schema.sections[0].elements).toHaveLength(2);
282
+ expect(schema.sections[0].elements[0].name).toBe('appName');
283
+ expect(schema.sections[0].elements[1].name).toBe('internalKey');
284
+ });
285
+ });
286
+ });
@@ -0,0 +1,119 @@
1
+ import { Section } from '@config-bound/config-bound/section';
2
+ import { Element } from '@config-bound/config-bound/element';
3
+ import Joi from 'joi';
4
+
5
+ /**
6
+ * Represents a single configuration element in the exported schema
7
+ */
8
+ export interface ExportedElement {
9
+ name: string;
10
+ description?: string;
11
+ type: string;
12
+ default?: unknown;
13
+ example?: unknown;
14
+ required: boolean;
15
+ sensitive: boolean;
16
+ joiValidation: unknown;
17
+ }
18
+
19
+ /**
20
+ * Represents a configuration section in the exported schema
21
+ */
22
+ export interface ExportedSection {
23
+ name: string;
24
+ description?: string;
25
+ elements: ExportedElement[];
26
+ }
27
+
28
+ /**
29
+ * The complete exported configuration schema
30
+ */
31
+ export interface ExportedSchema {
32
+ name: string;
33
+ sections: ExportedSection[];
34
+ }
35
+
36
+ /**
37
+ * Extracts type information from a Joi schema
38
+ */
39
+ function extractJoiType(validator: Joi.AnySchema): string {
40
+ const describe = validator.describe();
41
+
42
+ if (describe.type === 'alternatives') {
43
+ const types =
44
+ describe.matches?.map(
45
+ (m: Joi.Description) => m.schema?.type || 'unknown'
46
+ ) || [];
47
+ return types.join(' | ') || 'any';
48
+ }
49
+
50
+ if (describe.type === 'string' && describe.allow) {
51
+ const allowed = describe.allow;
52
+ if (Array.isArray(allowed) && allowed.length > 0) {
53
+ return allowed.map((v) => `'${v}'`).join(' | ');
54
+ }
55
+ }
56
+
57
+ return describe.type || 'unknown';
58
+ }
59
+
60
+ /**
61
+ * Extracts the raw Joi object from a schema
62
+ */
63
+ function extractJoiValidation(validator: Joi.AnySchema): unknown {
64
+ return validator.describe();
65
+ }
66
+
67
+ /**
68
+ * Exports an Element to the external schema format
69
+ */
70
+ export function exportElement(element: Element<unknown>): ExportedElement {
71
+ return {
72
+ name: element.name,
73
+ description: element.description,
74
+ type: extractJoiType(element.validator),
75
+ default: element.default,
76
+ example: element.example,
77
+ required: element.isRequired(),
78
+ sensitive: element.sensitive,
79
+ joiValidation: extractJoiValidation(element.validator)
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Exports a Section to the external schema format
85
+ * @param section - The section to export
86
+ * @param includeOmitted - Whether to include elements marked with omitFromSchema (default: false)
87
+ */
88
+ export function exportSection(
89
+ section: Section,
90
+ includeOmitted: boolean = false
91
+ ): ExportedSection {
92
+ return {
93
+ name: section.name,
94
+ description: section.description,
95
+ elements: section
96
+ .getElements()
97
+ .filter(
98
+ (element: Element<unknown>) => includeOmitted || !element.omitFromSchema
99
+ )
100
+ .map(exportElement)
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Exports the complete schema to a structured format
106
+ * @param name - The name of the configuration
107
+ * @param sections - Array of sections to export
108
+ * @param includeOmitted - Whether to include elements marked with omitFromSchema (default: false)
109
+ */
110
+ export function exportSchema(
111
+ name: string,
112
+ sections: Section[],
113
+ includeOmitted: boolean = false
114
+ ): ExportedSchema {
115
+ return {
116
+ name,
117
+ sections: sections.map((section) => exportSection(section, includeOmitted))
118
+ };
119
+ }