@celilo/cli 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.
- package/README.md +1566 -0
- package/bin/celilo +16 -0
- package/drizzle/0000_complex_puma.sql +179 -0
- package/drizzle/0001_dizzy_wolfpack.sql +2 -0
- package/drizzle/0002_web_routes.sql +16 -0
- package/drizzle/0003_backup_storage.sql +32 -0
- package/drizzle/meta/0000_snapshot.json +1151 -0
- package/drizzle/meta/0001_snapshot.json +1167 -0
- package/drizzle/meta/0002_snapshot.json +1257 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +64 -0
- package/schemas/system_config.json +106 -0
- package/src/__integration__/container-services-cli.integration.test.ts +246 -0
- package/src/ansible/dependencies.test.ts +309 -0
- package/src/ansible/dependencies.ts +896 -0
- package/src/ansible/inventory.test.ts +463 -0
- package/src/ansible/inventory.ts +445 -0
- package/src/ansible/secrets.ts +222 -0
- package/src/ansible/validation.test.ts +92 -0
- package/src/ansible/validation.ts +272 -0
- package/src/api-clients/digitalocean.ts +94 -0
- package/src/api-clients/proxmox.ts +655 -0
- package/src/capabilities/logging-wrapper.test.ts +217 -0
- package/src/capabilities/lookup.test.ts +149 -0
- package/src/capabilities/lookup.ts +89 -0
- package/src/capabilities/public-web-helpers.test.ts +198 -0
- package/src/capabilities/public-web-publish.test.ts +458 -0
- package/src/capabilities/registration.test.ts +395 -0
- package/src/capabilities/registration.ts +200 -0
- package/src/capabilities/route-validation.test.ts +121 -0
- package/src/capabilities/route-validation.ts +96 -0
- package/src/capabilities/secret-ref.test.ts +313 -0
- package/src/capabilities/secret-validation.ts +157 -0
- package/src/capabilities/secrets.test.ts +750 -0
- package/src/capabilities/secrets.ts +244 -0
- package/src/capabilities/validation.test.ts +613 -0
- package/src/capabilities/validation.ts +160 -0
- package/src/capabilities/well-known.test.ts +238 -0
- package/src/capabilities/well-known.ts +222 -0
- package/src/cli/cli.test.ts +654 -0
- package/src/cli/command-registry.ts +742 -0
- package/src/cli/command-tree-parser.test.ts +180 -0
- package/src/cli/command-tree-parser.ts +193 -0
- package/src/cli/commands/backup-create.ts +137 -0
- package/src/cli/commands/backup-delete.ts +74 -0
- package/src/cli/commands/backup-import.ts +97 -0
- package/src/cli/commands/backup-list.ts +132 -0
- package/src/cli/commands/backup-name.ts +73 -0
- package/src/cli/commands/backup-prune.ts +98 -0
- package/src/cli/commands/backup-restore.ts +122 -0
- package/src/cli/commands/capability-info.ts +121 -0
- package/src/cli/commands/capability-list.ts +47 -0
- package/src/cli/commands/completion.ts +87 -0
- package/src/cli/commands/hook-run.ts +176 -0
- package/src/cli/commands/ipam.ts +607 -0
- package/src/cli/commands/machine-add.ts +235 -0
- package/src/cli/commands/machine-earmark.ts +82 -0
- package/src/cli/commands/machine-list.ts +77 -0
- package/src/cli/commands/machine-remove.ts +90 -0
- package/src/cli/commands/machine-status.ts +131 -0
- package/src/cli/commands/module-audit.ts +51 -0
- package/src/cli/commands/module-build.ts +60 -0
- package/src/cli/commands/module-config.ts +170 -0
- package/src/cli/commands/module-deploy.ts +71 -0
- package/src/cli/commands/module-generate.ts +236 -0
- package/src/cli/commands/module-health.ts +108 -0
- package/src/cli/commands/module-import.ts +80 -0
- package/src/cli/commands/module-list.ts +43 -0
- package/src/cli/commands/module-logs.ts +73 -0
- package/src/cli/commands/module-remove.ts +162 -0
- package/src/cli/commands/module-show.ts +208 -0
- package/src/cli/commands/module-status.ts +131 -0
- package/src/cli/commands/module-types.ts +189 -0
- package/src/cli/commands/module-upgrade.ts +192 -0
- package/src/cli/commands/package.ts +68 -0
- package/src/cli/commands/secret-list.ts +99 -0
- package/src/cli/commands/secret-set.ts +134 -0
- package/src/cli/commands/service-add-digitalocean.ts +133 -0
- package/src/cli/commands/service-add-proxmox.ts +342 -0
- package/src/cli/commands/service-config-get.ts +83 -0
- package/src/cli/commands/service-config-set.ts +145 -0
- package/src/cli/commands/service-list.ts +74 -0
- package/src/cli/commands/service-reconfigure.ts +230 -0
- package/src/cli/commands/service-remove.ts +103 -0
- package/src/cli/commands/service-verify.ts +240 -0
- package/src/cli/commands/status.ts +216 -0
- package/src/cli/commands/storage-add-local.ts +106 -0
- package/src/cli/commands/storage-add-s3.ts +114 -0
- package/src/cli/commands/storage-list.ts +72 -0
- package/src/cli/commands/storage-remove.ts +54 -0
- package/src/cli/commands/storage-set-default.ts +44 -0
- package/src/cli/commands/storage-verify.ts +54 -0
- package/src/cli/commands/system-config.ts +168 -0
- package/src/cli/commands/system-init.ts +314 -0
- package/src/cli/commands/system-secret-get.ts +98 -0
- package/src/cli/commands/system-secret-set.ts +76 -0
- package/src/cli/commands/system-vault-password.ts +34 -0
- package/src/cli/completion.test.ts +37 -0
- package/src/cli/completion.ts +482 -0
- package/src/cli/fuel-gauge.test.ts +208 -0
- package/src/cli/fuel-gauge.ts +405 -0
- package/src/cli/generate-zsh-completion.test.ts +95 -0
- package/src/cli/generate-zsh-completion.ts +497 -0
- package/src/cli/index.ts +1583 -0
- package/src/cli/interactive-config.test.ts +201 -0
- package/src/cli/interactive-config.ts +62 -0
- package/src/cli/parser.test.ts +227 -0
- package/src/cli/parser.ts +244 -0
- package/src/cli/prompts.test.ts +33 -0
- package/src/cli/prompts.ts +121 -0
- package/src/cli/types.ts +38 -0
- package/src/cli/validators.test.ts +235 -0
- package/src/cli/validators.ts +188 -0
- package/src/config/env.ts +41 -0
- package/src/config/paths.test.ts +172 -0
- package/src/config/paths.ts +108 -0
- package/src/db/client.ts +190 -0
- package/src/db/migrate.ts +30 -0
- package/src/db/schema.test.ts +221 -0
- package/src/db/schema.ts +434 -0
- package/src/hooks/capability-loader-firewall.test.ts +246 -0
- package/src/hooks/capability-loader.test.ts +100 -0
- package/src/hooks/capability-loader.ts +520 -0
- package/src/hooks/define-hook.test.ts +488 -0
- package/src/hooks/executor.test.ts +462 -0
- package/src/hooks/executor.ts +469 -0
- package/src/hooks/logger.test.ts +54 -0
- package/src/hooks/logger.ts +95 -0
- package/src/hooks/test-fixtures/failing-hook.ts +13 -0
- package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
- package/src/hooks/test-fixtures/success-hook.ts +20 -0
- package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
- package/src/hooks/test-fixtures/void-hook.ts +13 -0
- package/src/hooks/types.ts +89 -0
- package/src/infrastructure/property-extractor.test.ts +194 -0
- package/src/infrastructure/property-extractor.ts +151 -0
- package/src/ipam/allocator.test.ts +442 -0
- package/src/ipam/allocator.ts +369 -0
- package/src/ipam/auto-allocator.test.ts +247 -0
- package/src/ipam/auto-allocator.ts +270 -0
- package/src/ipam/subnet-parser.test.ts +107 -0
- package/src/ipam/subnet-parser.ts +136 -0
- package/src/manifest/contracts/index.ts +61 -0
- package/src/manifest/contracts/v1.ts +118 -0
- package/src/manifest/json-schema-roundtrip.test.ts +99 -0
- package/src/manifest/schema.ts +367 -0
- package/src/manifest/template-validator.test.ts +231 -0
- package/src/manifest/template-validator.ts +322 -0
- package/src/manifest/validate.test.ts +1180 -0
- package/src/manifest/validate.ts +415 -0
- package/src/module/import.test.ts +355 -0
- package/src/module/import.ts +676 -0
- package/src/module/packaging/audit.ts +169 -0
- package/src/module/packaging/build.ts +228 -0
- package/src/module/packaging/checksum.ts +41 -0
- package/src/module/packaging/extract.ts +234 -0
- package/src/module/packaging/signature.ts +47 -0
- package/src/secrets/encryption.test.ts +284 -0
- package/src/secrets/encryption.ts +162 -0
- package/src/secrets/generators.test.ts +112 -0
- package/src/secrets/generators.ts +127 -0
- package/src/secrets/master-key.test.ts +159 -0
- package/src/secrets/master-key.ts +114 -0
- package/src/secrets/storage.test.ts +115 -0
- package/src/secrets/storage.ts +106 -0
- package/src/secrets/vault.test.ts +35 -0
- package/src/secrets/vault.ts +42 -0
- package/src/services/backup-create.ts +532 -0
- package/src/services/backup-metadata.ts +198 -0
- package/src/services/backup-restore.ts +229 -0
- package/src/services/backup-retention.ts +84 -0
- package/src/services/backup-storage.ts +281 -0
- package/src/services/build-stream.test.ts +122 -0
- package/src/services/build-stream.ts +201 -0
- package/src/services/config-interview.ts +694 -0
- package/src/services/container-service.test.ts +298 -0
- package/src/services/container-service.ts +401 -0
- package/src/services/cross-module-data-manager.test.ts +405 -0
- package/src/services/cross-module-data-manager.ts +412 -0
- package/src/services/deploy-ansible.ts +88 -0
- package/src/services/deploy-planner.ts +153 -0
- package/src/services/deploy-preflight.ts +274 -0
- package/src/services/deploy-ssh.ts +131 -0
- package/src/services/deploy-terraform.test.ts +55 -0
- package/src/services/deploy-terraform.ts +445 -0
- package/src/services/deploy-validation.ts +311 -0
- package/src/services/dns-auto-register.ts +211 -0
- package/src/services/health-runner.ts +184 -0
- package/src/services/infrastructure-selector.test.ts +485 -0
- package/src/services/infrastructure-selector.ts +245 -0
- package/src/services/infrastructure-variable-resolver.test.ts +751 -0
- package/src/services/infrastructure-variable-resolver.ts +234 -0
- package/src/services/machine-detector.ts +328 -0
- package/src/services/machine-pool.test.ts +405 -0
- package/src/services/machine-pool.ts +316 -0
- package/src/services/manifest-validation.ts +120 -0
- package/src/services/module-build.test.ts +290 -0
- package/src/services/module-build.ts +431 -0
- package/src/services/module-config.test.ts +237 -0
- package/src/services/module-config.ts +298 -0
- package/src/services/module-deploy.ts +862 -0
- package/src/services/module-types-drift.test.ts +73 -0
- package/src/services/module-types-generator.test.ts +288 -0
- package/src/services/module-types-generator.ts +189 -0
- package/src/services/proxmox-state-recovery.ts +140 -0
- package/src/services/schema-validation.ts +155 -0
- package/src/services/secret-schema-loader.test.ts +311 -0
- package/src/services/secret-schema-loader.ts +239 -0
- package/src/services/ssh-key-manager.test.ts +283 -0
- package/src/services/ssh-key-manager.ts +193 -0
- package/src/services/storage-providers/local.ts +105 -0
- package/src/services/storage-providers/s3.ts +182 -0
- package/src/services/storage-providers/types.ts +24 -0
- package/src/services/system-config-schema-types.ts +25 -0
- package/src/services/system-config-validator.test.ts +160 -0
- package/src/services/system-config-validator.ts +74 -0
- package/src/services/system-init.test.ts +153 -0
- package/src/services/system-init.ts +253 -0
- package/src/services/terraform-safety.ts +174 -0
- package/src/services/zone-detector.test.ts +110 -0
- package/src/services/zone-detector.ts +102 -0
- package/src/services/zone-policy.test.ts +97 -0
- package/src/services/zone-policy.ts +126 -0
- package/src/templates/generator.test.ts +645 -0
- package/src/templates/generator.ts +1119 -0
- package/src/templates/types.ts +62 -0
- package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
- package/src/test-utils/cli-context-interactive.test.ts +152 -0
- package/src/test-utils/cli-context-server.test.ts +66 -0
- package/src/test-utils/cli-context.test.ts +273 -0
- package/src/test-utils/cli-context.ts +677 -0
- package/src/test-utils/cli-result.test.ts +282 -0
- package/src/test-utils/cli-result.ts +241 -0
- package/src/test-utils/cli.ts +55 -0
- package/src/test-utils/completion-harness.test.ts +126 -0
- package/src/test-utils/completion-harness.ts +82 -0
- package/src/test-utils/database.test.ts +182 -0
- package/src/test-utils/database.ts +126 -0
- package/src/test-utils/filesystem.test.ts +208 -0
- package/src/test-utils/filesystem.ts +142 -0
- package/src/test-utils/fixtures.test.ts +123 -0
- package/src/test-utils/fixtures.ts +160 -0
- package/src/test-utils/golden-diff.ts +197 -0
- package/src/test-utils/index.ts +77 -0
- package/src/test-utils/integration.ts +81 -0
- package/src/test-utils/module-fixtures.ts +468 -0
- package/src/test-utils/modules.test.ts +144 -0
- package/src/test-utils/modules.ts +183 -0
- package/src/test-utils/setup-test-db.ts +90 -0
- package/src/test-utils/value-extractor.test.ts +231 -0
- package/src/test-utils/value-extractor.ts +228 -0
- package/src/types/infrastructure.ts +157 -0
- package/src/utils/shell.test.ts +365 -0
- package/src/utils/shell.ts +159 -0
- package/src/validation/schemas.ts +166 -0
- package/src/variables/ansible-resolver.test.ts +142 -0
- package/src/variables/ansible-resolver.ts +69 -0
- package/src/variables/capability-self-ref.test.ts +220 -0
- package/src/variables/context.test.ts +1265 -0
- package/src/variables/context.ts +624 -0
- package/src/variables/declarative-derivation.test.ts +743 -0
- package/src/variables/declarative-derivation.ts +200 -0
- package/src/variables/parser.test.ts +231 -0
- package/src/variables/parser.ts +76 -0
- package/src/variables/resolver.test.ts +458 -0
- package/src/variables/resolver.ts +282 -0
- package/src/variables/types.ts +59 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for module configuration service
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
6
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { closeDb, createDbClient } from '../db/client';
|
|
10
|
+
import { runMigrations } from '../db/migrate';
|
|
11
|
+
import { modules } from '../db/schema';
|
|
12
|
+
import {
|
|
13
|
+
formatConfigValue,
|
|
14
|
+
getAllModuleConfigValues,
|
|
15
|
+
getModuleConfigValue,
|
|
16
|
+
isComplexValue,
|
|
17
|
+
parseConfigValue,
|
|
18
|
+
setModuleConfigValue,
|
|
19
|
+
} from './module-config';
|
|
20
|
+
|
|
21
|
+
describe('module-config service', () => {
|
|
22
|
+
let testDbPath: string;
|
|
23
|
+
let testDir: string;
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
// Create temp directory for test database
|
|
27
|
+
testDir = mkdtempSync(join(tmpdir(), 'celilo-test-'));
|
|
28
|
+
testDbPath = join(testDir, 'test.db');
|
|
29
|
+
|
|
30
|
+
// Set environment variable for database path
|
|
31
|
+
process.env.CELILO_DB_PATH = testDbPath;
|
|
32
|
+
|
|
33
|
+
// Initialize database and run migrations
|
|
34
|
+
await runMigrations(testDbPath);
|
|
35
|
+
|
|
36
|
+
// Insert test module
|
|
37
|
+
const db = createDbClient({ path: testDbPath });
|
|
38
|
+
db.insert(modules)
|
|
39
|
+
.values({
|
|
40
|
+
id: 'test-module',
|
|
41
|
+
name: 'Test Module',
|
|
42
|
+
version: '1.0.0',
|
|
43
|
+
manifestData: {
|
|
44
|
+
variables: {
|
|
45
|
+
owns: [
|
|
46
|
+
{ name: 'string_var', type: 'string' },
|
|
47
|
+
{ name: 'number_var', type: 'number' },
|
|
48
|
+
{ name: 'bool_var', type: 'boolean' },
|
|
49
|
+
{ name: 'array_var', type: 'array' },
|
|
50
|
+
{ name: 'object_var', type: 'object' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
sourcePath: '/test/path',
|
|
55
|
+
})
|
|
56
|
+
.run();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
closeDb();
|
|
61
|
+
// Clean up environment variable
|
|
62
|
+
process.env.CELILO_DB_PATH = undefined;
|
|
63
|
+
// Clean up test database
|
|
64
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('isComplexValue', () => {
|
|
68
|
+
it('should return false for primitives', () => {
|
|
69
|
+
expect(isComplexValue('string')).toBe(false);
|
|
70
|
+
expect(isComplexValue(123)).toBe(false);
|
|
71
|
+
expect(isComplexValue(true)).toBe(false);
|
|
72
|
+
expect(isComplexValue(null)).toBe(false);
|
|
73
|
+
expect(isComplexValue(undefined)).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return true for arrays', () => {
|
|
77
|
+
expect(isComplexValue([])).toBe(true);
|
|
78
|
+
expect(isComplexValue([1, 2, 3])).toBe(true);
|
|
79
|
+
expect(isComplexValue(['a', 'b'])).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should return true for objects', () => {
|
|
83
|
+
expect(isComplexValue({})).toBe(false); // Empty object
|
|
84
|
+
expect(isComplexValue({ key: 'value' })).toBe(true);
|
|
85
|
+
expect(isComplexValue({ a: 1, b: 2 })).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('parseConfigValue', () => {
|
|
90
|
+
it('should parse primitives from strings', async () => {
|
|
91
|
+
expect(await parseConfigValue('hello')).toBe('hello');
|
|
92
|
+
expect(await parseConfigValue('123')).toBe(123);
|
|
93
|
+
expect(await parseConfigValue('true')).toBe(true);
|
|
94
|
+
expect(await parseConfigValue('false')).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should parse JSON strings', async () => {
|
|
98
|
+
expect(await parseConfigValue('{"key":"value"}')).toEqual({ key: 'value' });
|
|
99
|
+
expect(await parseConfigValue('[1,2,3]')).toEqual([1, 2, 3]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should read from @file syntax', async () => {
|
|
103
|
+
const filePath = join(testDir, 'config.json');
|
|
104
|
+
writeFileSync(filePath, JSON.stringify({ key: 'value' }));
|
|
105
|
+
|
|
106
|
+
const result = await parseConfigValue(`@${filePath}`);
|
|
107
|
+
expect(result).toEqual({ key: 'value' });
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('setModuleConfigValue and getModuleConfigValue', () => {
|
|
112
|
+
it('should store and retrieve primitive string', async () => {
|
|
113
|
+
await setModuleConfigValue('test-module', 'string_var', 'hello');
|
|
114
|
+
|
|
115
|
+
const result = getModuleConfigValue('test-module', 'string_var');
|
|
116
|
+
expect(result).not.toBeNull();
|
|
117
|
+
expect(result?.value).toBe('hello');
|
|
118
|
+
expect(result?.isPrimitive).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should store and retrieve primitive number', async () => {
|
|
122
|
+
await setModuleConfigValue('test-module', 'number_var', '42');
|
|
123
|
+
|
|
124
|
+
const result = getModuleConfigValue('test-module', 'number_var');
|
|
125
|
+
expect(result).not.toBeNull();
|
|
126
|
+
expect(result?.value).toBe(42);
|
|
127
|
+
expect(result?.isPrimitive).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should store and retrieve primitive boolean', async () => {
|
|
131
|
+
await setModuleConfigValue('test-module', 'bool_var', 'true');
|
|
132
|
+
|
|
133
|
+
const result = getModuleConfigValue('test-module', 'bool_var');
|
|
134
|
+
expect(result).not.toBeNull();
|
|
135
|
+
expect(result?.value).toBe(true);
|
|
136
|
+
expect(result?.isPrimitive).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should store and retrieve complex array', async () => {
|
|
140
|
+
const arrayValue = JSON.stringify([1, 2, 3]);
|
|
141
|
+
await setModuleConfigValue('test-module', 'array_var', arrayValue);
|
|
142
|
+
|
|
143
|
+
const result = getModuleConfigValue('test-module', 'array_var');
|
|
144
|
+
expect(result).not.toBeNull();
|
|
145
|
+
expect(result?.value).toEqual([1, 2, 3]);
|
|
146
|
+
expect(result?.isPrimitive).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should store and retrieve complex object', async () => {
|
|
150
|
+
const objectValue = JSON.stringify({ key: 'value', nested: { foo: 'bar' } });
|
|
151
|
+
await setModuleConfigValue('test-module', 'object_var', objectValue);
|
|
152
|
+
|
|
153
|
+
const result = getModuleConfigValue('test-module', 'object_var');
|
|
154
|
+
expect(result).not.toBeNull();
|
|
155
|
+
expect(result?.value).toEqual({ key: 'value', nested: { foo: 'bar' } });
|
|
156
|
+
expect(result?.isPrimitive).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should update existing config', async () => {
|
|
160
|
+
await setModuleConfigValue('test-module', 'string_var', 'first');
|
|
161
|
+
await setModuleConfigValue('test-module', 'string_var', 'second');
|
|
162
|
+
|
|
163
|
+
const result = getModuleConfigValue('test-module', 'string_var');
|
|
164
|
+
expect(result?.value).toBe('second');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should reject type mismatch based on manifest (primitive to complex)', async () => {
|
|
168
|
+
// array_var is declared as type: array in manifest
|
|
169
|
+
// Setting it to a string should fail manifest validation
|
|
170
|
+
await expect(
|
|
171
|
+
setModuleConfigValue('test-module', 'array_var', 'simple string'),
|
|
172
|
+
).rejects.toThrow('Expected array');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should reject type mismatch based on manifest (complex to primitive)', async () => {
|
|
176
|
+
// string_var is declared as type: string in manifest
|
|
177
|
+
// Setting it to an object (via JSON) should fail manifest validation
|
|
178
|
+
await expect(
|
|
179
|
+
setModuleConfigValue('test-module', 'string_var', '{"key":"value"}'),
|
|
180
|
+
).rejects.toThrow('Expected string');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('getAllModuleConfigValues', () => {
|
|
185
|
+
it('should return empty array when no config exists', () => {
|
|
186
|
+
const result = getAllModuleConfigValues('test-module');
|
|
187
|
+
expect(result).toEqual([]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should return all config values', async () => {
|
|
191
|
+
await setModuleConfigValue('test-module', 'string_var', 'hello');
|
|
192
|
+
await setModuleConfigValue('test-module', 'number_var', '42');
|
|
193
|
+
await setModuleConfigValue('test-module', 'array_var', '[1,2,3]');
|
|
194
|
+
|
|
195
|
+
const result = getAllModuleConfigValues('test-module');
|
|
196
|
+
expect(result).toHaveLength(3);
|
|
197
|
+
|
|
198
|
+
const stringVar = result.find((c) => c.key === 'string_var');
|
|
199
|
+
expect(stringVar?.value).toBe('hello');
|
|
200
|
+
expect(stringVar?.isPrimitive).toBe(true);
|
|
201
|
+
|
|
202
|
+
const numberVar = result.find((c) => c.key === 'number_var');
|
|
203
|
+
expect(numberVar?.value).toBe(42);
|
|
204
|
+
expect(numberVar?.isPrimitive).toBe(true);
|
|
205
|
+
|
|
206
|
+
const arrayVar = result.find((c) => c.key === 'array_var');
|
|
207
|
+
expect(arrayVar?.value).toEqual([1, 2, 3]);
|
|
208
|
+
expect(arrayVar?.isPrimitive).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('formatConfigValue', () => {
|
|
213
|
+
it('should format primitive values', () => {
|
|
214
|
+
expect(formatConfigValue({ key: 'test', value: 'hello', isPrimitive: true })).toBe('hello');
|
|
215
|
+
expect(formatConfigValue({ key: 'test', value: 42, isPrimitive: true })).toBe('42');
|
|
216
|
+
expect(formatConfigValue({ key: 'test', value: true, isPrimitive: true })).toBe('true');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should format complex values as JSON', () => {
|
|
220
|
+
const result = formatConfigValue({
|
|
221
|
+
key: 'test',
|
|
222
|
+
value: [1, 2, 3],
|
|
223
|
+
isPrimitive: false,
|
|
224
|
+
});
|
|
225
|
+
expect(result).toBe('[\n 1,\n 2,\n 3\n]');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should format object values as JSON', () => {
|
|
229
|
+
const result = formatConfigValue({
|
|
230
|
+
key: 'test',
|
|
231
|
+
value: { key: 'value' },
|
|
232
|
+
isPrimitive: false,
|
|
233
|
+
});
|
|
234
|
+
expect(result).toBe('{\n "key": "value"\n}');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module configuration service
|
|
3
|
+
* Handles storage and retrieval of module configuration values
|
|
4
|
+
* Supports both primitive types (string, number, boolean) and complex types (arrays, objects)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import { and, eq } from 'drizzle-orm';
|
|
9
|
+
import { type DbClient, getDb } from '../db/client';
|
|
10
|
+
import { moduleConfigs, modules } from '../db/schema';
|
|
11
|
+
import { formatManifestValidationErrors, validateAgainstManifest } from './manifest-validation';
|
|
12
|
+
import { formatValidationErrors, validateConfigValue } from './schema-validation';
|
|
13
|
+
|
|
14
|
+
export interface ConfigValue {
|
|
15
|
+
key: string;
|
|
16
|
+
value: string | number | boolean | unknown[] | Record<string, unknown>;
|
|
17
|
+
isPrimitive: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if a value is a complex type (array or object)
|
|
22
|
+
*/
|
|
23
|
+
export function isComplexValue(value: unknown): boolean {
|
|
24
|
+
if (value === null || typeof value !== 'object') {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Arrays are complex
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Non-empty objects are complex
|
|
34
|
+
return Object.keys(value).length > 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse a configuration value
|
|
39
|
+
* Handles @file syntax for reading from files
|
|
40
|
+
* Handles JSON strings for complex types
|
|
41
|
+
*/
|
|
42
|
+
export async function parseConfigValue(
|
|
43
|
+
valueStr: string,
|
|
44
|
+
): Promise<string | number | boolean | unknown[] | Record<string, unknown>> {
|
|
45
|
+
// Handle @file syntax
|
|
46
|
+
if (valueStr.startsWith('@')) {
|
|
47
|
+
const filePath = valueStr.slice(1);
|
|
48
|
+
const fileContent = await readFile(filePath, 'utf-8');
|
|
49
|
+
return JSON.parse(fileContent);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Try to parse as JSON (for complex types or JSON strings)
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(valueStr);
|
|
55
|
+
return parsed;
|
|
56
|
+
} catch {
|
|
57
|
+
// Not valid JSON, treat as string
|
|
58
|
+
// Check for boolean values
|
|
59
|
+
if (valueStr === 'true') return true;
|
|
60
|
+
if (valueStr === 'false') return false;
|
|
61
|
+
|
|
62
|
+
// Check for number values
|
|
63
|
+
const num = Number(valueStr);
|
|
64
|
+
if (!Number.isNaN(num) && valueStr.trim() !== '') {
|
|
65
|
+
return num;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Return as string
|
|
69
|
+
return valueStr;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get module configuration value
|
|
75
|
+
* Returns parsed value (primitive or complex type)
|
|
76
|
+
*/
|
|
77
|
+
export function getModuleConfigValue(
|
|
78
|
+
moduleId: string,
|
|
79
|
+
key: string,
|
|
80
|
+
db: DbClient = getDb(),
|
|
81
|
+
): ConfigValue | null {
|
|
82
|
+
const config = db
|
|
83
|
+
.select()
|
|
84
|
+
.from(moduleConfigs)
|
|
85
|
+
.where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, key)))
|
|
86
|
+
.get();
|
|
87
|
+
|
|
88
|
+
if (!config) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check if complex type (stored in valueJson)
|
|
93
|
+
if (config.valueJson) {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(config.valueJson);
|
|
96
|
+
return {
|
|
97
|
+
key: config.key,
|
|
98
|
+
value: parsed,
|
|
99
|
+
isPrimitive: false,
|
|
100
|
+
};
|
|
101
|
+
} catch (error) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Failed to parse config value for ${moduleId}.${config.key}: ${error instanceof Error ? error.message : 'Invalid JSON'}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Primitive type (stored in value)
|
|
109
|
+
// Parse the string value to correct type
|
|
110
|
+
let parsedValue: string | number | boolean = config.value;
|
|
111
|
+
|
|
112
|
+
// Boolean
|
|
113
|
+
if (config.value === 'true') parsedValue = true;
|
|
114
|
+
else if (config.value === 'false') parsedValue = false;
|
|
115
|
+
// Number
|
|
116
|
+
else {
|
|
117
|
+
const num = Number(config.value);
|
|
118
|
+
if (!Number.isNaN(num)) {
|
|
119
|
+
parsedValue = num;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
key: config.key,
|
|
125
|
+
value: parsedValue,
|
|
126
|
+
isPrimitive: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get all module configuration values
|
|
132
|
+
*/
|
|
133
|
+
export function getAllModuleConfigValues(moduleId: string, db: DbClient = getDb()): ConfigValue[] {
|
|
134
|
+
const configs = db.select().from(moduleConfigs).where(eq(moduleConfigs.moduleId, moduleId)).all();
|
|
135
|
+
|
|
136
|
+
return configs.map((config: typeof moduleConfigs.$inferSelect) => {
|
|
137
|
+
// Check if complex type
|
|
138
|
+
if (config.valueJson) {
|
|
139
|
+
try {
|
|
140
|
+
const parsed = JSON.parse(config.valueJson);
|
|
141
|
+
return {
|
|
142
|
+
key: config.key,
|
|
143
|
+
value: parsed,
|
|
144
|
+
isPrimitive: false,
|
|
145
|
+
};
|
|
146
|
+
} catch (error) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Failed to parse config value for ${moduleId}.${config.key}: ${error instanceof Error ? error.message : 'Invalid JSON'}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Primitive type
|
|
154
|
+
let parsedValue: string | number | boolean = config.value;
|
|
155
|
+
|
|
156
|
+
// Boolean
|
|
157
|
+
if (config.value === 'true') parsedValue = true;
|
|
158
|
+
else if (config.value === 'false') parsedValue = false;
|
|
159
|
+
// Number
|
|
160
|
+
else {
|
|
161
|
+
const num = Number(config.value);
|
|
162
|
+
if (!Number.isNaN(num)) {
|
|
163
|
+
parsedValue = num;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
key: config.key,
|
|
169
|
+
value: parsedValue,
|
|
170
|
+
isPrimitive: true,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Set module configuration value
|
|
177
|
+
* Automatically detects primitive vs complex types and stores appropriately
|
|
178
|
+
*/
|
|
179
|
+
export async function setModuleConfigValue(
|
|
180
|
+
moduleId: string,
|
|
181
|
+
key: string,
|
|
182
|
+
valueStr: string,
|
|
183
|
+
db: DbClient = getDb(),
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
// Parse the value
|
|
186
|
+
const parsedValue = await parseConfigValue(valueStr);
|
|
187
|
+
|
|
188
|
+
// Get module manifest for validation
|
|
189
|
+
const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
|
|
190
|
+
if (!module) {
|
|
191
|
+
throw new Error(`Module not found: ${moduleId}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const manifest = module.manifestData as Record<string, unknown>;
|
|
195
|
+
const variables = manifest.variables as
|
|
196
|
+
| {
|
|
197
|
+
owns?: Array<{
|
|
198
|
+
name: string;
|
|
199
|
+
type?: string;
|
|
200
|
+
minimum?: number;
|
|
201
|
+
maximum?: number;
|
|
202
|
+
pattern?: string;
|
|
203
|
+
}>;
|
|
204
|
+
}
|
|
205
|
+
| undefined;
|
|
206
|
+
const declaredVars = variables?.owns || [];
|
|
207
|
+
const variable = declaredVars.find((v) => v.name === key);
|
|
208
|
+
|
|
209
|
+
// Validate against manifest type (if type is declared)
|
|
210
|
+
if (variable?.type) {
|
|
211
|
+
const manifestValidation = validateAgainstManifest(parsedValue, {
|
|
212
|
+
name: variable.name,
|
|
213
|
+
type: variable.type as 'string' | 'integer' | 'number' | 'boolean' | 'array' | 'object',
|
|
214
|
+
minimum: variable.minimum,
|
|
215
|
+
maximum: variable.maximum,
|
|
216
|
+
pattern: variable.pattern,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (!manifestValidation.valid) {
|
|
220
|
+
throw new Error(formatManifestValidationErrors(manifestValidation.errors || []));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Validate against JSON Schema (if schema exists)
|
|
225
|
+
// This provides deeper validation for complex types
|
|
226
|
+
const validation = await validateConfigValue(moduleId, key, parsedValue, db);
|
|
227
|
+
if (!validation.valid) {
|
|
228
|
+
throw new Error(formatValidationErrors(validation.errors || []));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Determine if complex type
|
|
232
|
+
const isComplex = isComplexValue(parsedValue);
|
|
233
|
+
|
|
234
|
+
// Check if config already exists
|
|
235
|
+
const existingConfig = db
|
|
236
|
+
.select()
|
|
237
|
+
.from(moduleConfigs)
|
|
238
|
+
.where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, key)))
|
|
239
|
+
.get();
|
|
240
|
+
|
|
241
|
+
if (existingConfig) {
|
|
242
|
+
// Update existing config
|
|
243
|
+
if (isComplex) {
|
|
244
|
+
// Store in valueJson column
|
|
245
|
+
db.update(moduleConfigs)
|
|
246
|
+
.set({
|
|
247
|
+
value: '', // Empty string for complex types
|
|
248
|
+
valueJson: JSON.stringify(parsedValue),
|
|
249
|
+
updatedAt: new Date(),
|
|
250
|
+
})
|
|
251
|
+
.where(eq(moduleConfigs.id, existingConfig.id))
|
|
252
|
+
.run();
|
|
253
|
+
} else {
|
|
254
|
+
// Store in value column
|
|
255
|
+
db.update(moduleConfigs)
|
|
256
|
+
.set({
|
|
257
|
+
value: String(parsedValue),
|
|
258
|
+
valueJson: null,
|
|
259
|
+
updatedAt: new Date(),
|
|
260
|
+
})
|
|
261
|
+
.where(eq(moduleConfigs.id, existingConfig.id))
|
|
262
|
+
.run();
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
// Insert new config
|
|
266
|
+
if (isComplex) {
|
|
267
|
+
db.insert(moduleConfigs)
|
|
268
|
+
.values({
|
|
269
|
+
moduleId,
|
|
270
|
+
key,
|
|
271
|
+
value: '', // Empty string for complex types
|
|
272
|
+
valueJson: JSON.stringify(parsedValue),
|
|
273
|
+
})
|
|
274
|
+
.run();
|
|
275
|
+
} else {
|
|
276
|
+
db.insert(moduleConfigs)
|
|
277
|
+
.values({
|
|
278
|
+
moduleId,
|
|
279
|
+
key,
|
|
280
|
+
value: String(parsedValue),
|
|
281
|
+
valueJson: null,
|
|
282
|
+
})
|
|
283
|
+
.run();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Format a config value for display
|
|
290
|
+
*/
|
|
291
|
+
export function formatConfigValue(configValue: ConfigValue): string {
|
|
292
|
+
if (configValue.isPrimitive) {
|
|
293
|
+
return String(configValue.value);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Complex type - format as JSON
|
|
297
|
+
return JSON.stringify(configValue.value, null, 2);
|
|
298
|
+
}
|