@zhin.js/gateway 0.0.1-bootstrap.0 → 1.1.2
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/LICENSE +21 -0
- package/README.md +61 -0
- package/lib/config/ai-config-loader.d.ts +22 -0
- package/lib/config/ai-config-loader.js +96 -0
- package/lib/config/configuration-chat-policy.d.ts +57 -0
- package/lib/config/configuration-chat-policy.js +423 -0
- package/lib/config/configuration-patch.d.ts +16 -0
- package/lib/config/configuration-patch.js +105 -0
- package/lib/config/configuration-path.d.ts +6 -0
- package/lib/config/configuration-path.js +40 -0
- package/lib/config/configuration-projection.d.ts +6 -0
- package/lib/config/configuration-projection.js +28 -0
- package/lib/config/configuration-redaction.d.ts +18 -0
- package/lib/config/configuration-redaction.js +75 -0
- package/lib/config/configuration.d.ts +81 -0
- package/lib/config/configuration.js +686 -0
- package/lib/config/console-schema.d.ts +11 -0
- package/lib/config/console-schema.js +223 -0
- package/lib/config/environment-files.d.ts +3 -0
- package/lib/config/environment-files.js +14 -0
- package/lib/config/literal-secrets.d.ts +34 -0
- package/lib/config/literal-secrets.js +136 -0
- package/lib/config/plugin-package-map.d.ts +2 -0
- package/lib/config/plugin-package-map.js +14 -0
- package/lib/config/plugin-schema-catalog.d.ts +22 -0
- package/lib/config/plugin-schema-catalog.js +262 -0
- package/lib/config.d.ts +13 -0
- package/lib/config.js +13 -0
- package/lib/database/config.d.ts +22 -0
- package/lib/database/config.js +159 -0
- package/lib/database/index.d.ts +2 -0
- package/lib/database/index.js +2 -0
- package/lib/database/project-database.d.ts +31 -0
- package/lib/database/project-database.js +130 -0
- package/lib/database.d.ts +2 -0
- package/lib/database.js +2 -0
- package/lib/http/console-rpc.d.ts +27 -0
- package/lib/http/console-rpc.js +51 -0
- package/lib/http/gateway-console.d.ts +63 -0
- package/lib/http/gateway-console.js +309 -0
- package/lib/http/gateway-events.d.ts +40 -0
- package/lib/http/gateway-events.js +185 -0
- package/lib/http/gateway-http-server.d.ts +95 -0
- package/lib/http/gateway-http-server.js +522 -0
- package/lib/http/local-principal.d.ts +2 -0
- package/lib/http/local-principal.js +16 -0
- package/lib/http/plugin-switch-rpc.d.ts +19 -0
- package/lib/http/plugin-switch-rpc.js +214 -0
- package/lib/http/run-gateway.d.ts +43 -0
- package/lib/http/run-gateway.js +268 -0
- package/lib/http/start-gateway.d.ts +114 -0
- package/lib/http/start-gateway.js +226 -0
- package/lib/http-response.d.ts +6 -0
- package/lib/http-response.js +25 -0
- package/lib/http.d.ts +6 -0
- package/lib/http.js +6 -0
- package/lib/iam/crypto.d.ts +12 -0
- package/lib/iam/crypto.js +58 -0
- package/lib/iam/index.d.ts +8 -0
- package/lib/iam/index.js +8 -0
- package/lib/iam/install.d.ts +36 -0
- package/lib/iam/install.js +74 -0
- package/lib/iam/local-admin.d.ts +27 -0
- package/lib/iam/local-admin.js +102 -0
- package/lib/iam/rate-limit.d.ts +15 -0
- package/lib/iam/rate-limit.js +52 -0
- package/lib/iam/routes.d.ts +23 -0
- package/lib/iam/routes.js +157 -0
- package/lib/iam/rpc.d.ts +14 -0
- package/lib/iam/rpc.js +100 -0
- package/lib/iam/service.d.ts +203 -0
- package/lib/iam/service.js +784 -0
- package/lib/iam/setup-service.d.ts +83 -0
- package/lib/iam/setup-service.js +206 -0
- package/lib/iam/tables.d.ts +20 -0
- package/lib/iam/tables.js +82 -0
- package/lib/iam.d.ts +2 -0
- package/lib/iam.js +2 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.js +22 -0
- package/lib/local/client.d.ts +71 -0
- package/lib/local/client.js +296 -0
- package/lib/local/handler.d.ts +30 -0
- package/lib/local/handler.js +86 -0
- package/lib/local/local-socket.d.ts +55 -0
- package/lib/local/local-socket.js +232 -0
- package/lib/local/paths.d.ts +34 -0
- package/lib/local/paths.js +58 -0
- package/lib/local/protocol.d.ts +140 -0
- package/lib/local/protocol.js +90 -0
- package/lib/local/state-file.d.ts +8 -0
- package/lib/local/state-file.js +45 -0
- package/lib/local.d.ts +10 -0
- package/lib/local.js +10 -0
- package/lib/onboarding/allowlist.d.ts +54 -0
- package/lib/onboarding/allowlist.js +106 -0
- package/lib/onboarding/errors.d.ts +7 -0
- package/lib/onboarding/errors.js +17 -0
- package/lib/onboarding/index.d.ts +6 -0
- package/lib/onboarding/index.js +6 -0
- package/lib/onboarding/install.d.ts +99 -0
- package/lib/onboarding/install.js +132 -0
- package/lib/onboarding/package-manager.d.ts +47 -0
- package/lib/onboarding/package-manager.js +179 -0
- package/lib/onboarding/service.d.ts +53 -0
- package/lib/onboarding/service.js +144 -0
- package/lib/onboarding/state-store.d.ts +26 -0
- package/lib/onboarding/state-store.js +97 -0
- package/lib/onboarding.d.ts +2 -0
- package/lib/onboarding.js +2 -0
- package/lib/packages/authorization.d.ts +24 -0
- package/lib/packages/authorization.js +128 -0
- package/lib/packages/errors.d.ts +11 -0
- package/lib/packages/errors.js +26 -0
- package/lib/packages/index.d.ts +5 -0
- package/lib/packages/index.js +5 -0
- package/lib/packages/rpc.d.ts +30 -0
- package/lib/packages/rpc.js +160 -0
- package/lib/packages/service.d.ts +139 -0
- package/lib/packages/service.js +504 -0
- package/lib/packages/snapshot.d.ts +14 -0
- package/lib/packages/snapshot.js +73 -0
- package/lib/packages.d.ts +2 -0
- package/lib/packages.js +2 -0
- package/lib/plugin-management/activity.d.ts +6 -0
- package/lib/plugin-management/activity.js +14 -0
- package/lib/plugin-management/config-explanation.d.ts +6 -0
- package/lib/plugin-management/config-explanation.js +113 -0
- package/lib/plugin-management/use-cases.d.ts +15 -0
- package/lib/plugin-management/use-cases.js +120 -0
- package/lib/plugin-management/workflow.d.ts +136 -0
- package/lib/plugin-management/workflow.js +878 -0
- package/lib/plugin-management.d.ts +5 -0
- package/lib/plugin-management.js +5 -0
- package/lib/project/config-file.d.ts +6 -0
- package/lib/project/config-file.js +58 -0
- package/lib/project/environment.d.ts +13 -0
- package/lib/project/environment.js +52 -0
- package/lib/project/instance-declarations.d.ts +55 -0
- package/lib/project/instance-declarations.js +542 -0
- package/lib/project/legacy-config.d.ts +25 -0
- package/lib/project/legacy-config.js +70 -0
- package/lib/project/node-requirements.d.ts +7 -0
- package/lib/project/node-requirements.js +20 -0
- package/lib/project/tsx-module-loader.d.ts +7 -0
- package/lib/project/tsx-module-loader.js +29 -0
- package/lib/project.d.ts +7 -0
- package/lib/project.js +7 -0
- package/lib/server.d.ts +11 -0
- package/lib/server.js +11 -0
- package/lib/supervisor/instance-logs.d.ts +39 -0
- package/lib/supervisor/instance-logs.js +152 -0
- package/lib/supervisor/instance-supervisor.d.ts +140 -0
- package/lib/supervisor/instance-supervisor.js +408 -0
- package/lib/supervisor/run-instance.d.ts +35 -0
- package/lib/supervisor/run-instance.js +161 -0
- package/lib/supervisor/terminal-state.d.ts +30 -0
- package/lib/supervisor/terminal-state.js +57 -0
- package/lib/supervisor.d.ts +4 -0
- package/lib/supervisor.js +4 -0
- package/lib/terminal/backends.d.ts +38 -0
- package/lib/terminal/backends.js +193 -0
- package/lib/terminal/config.d.ts +63 -0
- package/lib/terminal/config.js +237 -0
- package/lib/terminal/host.d.ts +26 -0
- package/lib/terminal/host.js +76 -0
- package/lib/terminal/index.d.ts +60 -0
- package/lib/terminal/index.js +135 -0
- package/lib/terminal/modules.d.ts +87 -0
- package/lib/terminal/modules.js +44 -0
- package/lib/terminal/service.d.ts +101 -0
- package/lib/terminal/service.js +455 -0
- package/lib/terminal/tickets.d.ts +46 -0
- package/lib/terminal/tickets.js +76 -0
- package/lib/terminal.d.ts +3 -0
- package/lib/terminal.js +3 -0
- package/lib/usage/aggregate.d.ts +87 -0
- package/lib/usage/aggregate.js +212 -0
- package/lib/usage.d.ts +2 -0
- package/lib/usage.js +2 -0
- package/package.json +117 -4
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createConfigDocument, HOST_CONFIG_KEYS, readApplicationPluginDeclaration, HOST_CONFIG_SCHEMA, ManifestValidationError, NodePackageResolver, PackageResolutionError, } from '@zhin.js/runtime';
|
|
4
|
+
import { findConfigFile } from '../project/config-file.js';
|
|
5
|
+
import { isConsoleSchemaJson, jsonSchemaToConsoleSchema } from './console-schema.js';
|
|
6
|
+
const HOST_CONFIG_KEY_SET = new Set(HOST_CONFIG_KEYS);
|
|
7
|
+
/** Resolves schemas using the same package roots and declarations as Runtime. */
|
|
8
|
+
export class PluginSchemaCatalog {
|
|
9
|
+
#projectRoot;
|
|
10
|
+
#declarations;
|
|
11
|
+
constructor(projectRoot, declarations) {
|
|
12
|
+
this.#projectRoot = projectRoot;
|
|
13
|
+
this.#declarations = declarations;
|
|
14
|
+
}
|
|
15
|
+
async read(pluginName, document) {
|
|
16
|
+
if (!pluginName)
|
|
17
|
+
return null;
|
|
18
|
+
return this.#read(pluginName, () => NodePackageResolver.create(this.#projectRoot), await this.#declarations?.(), false, document);
|
|
19
|
+
}
|
|
20
|
+
async readJson(pluginName, document) {
|
|
21
|
+
return (await this.#read(pluginName, () => NodePackageResolver.create(this.#projectRoot), await this.#declarations?.(), true, document));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Raw JSON schemas for several keys sharing one resolver and one declaration read.
|
|
25
|
+
* A key whose schema cannot be read maps to its `Error`, so callers can fail closed per key.
|
|
26
|
+
*/
|
|
27
|
+
async readJsonMany(keys, document) {
|
|
28
|
+
const list = [...keys];
|
|
29
|
+
let declarations;
|
|
30
|
+
try {
|
|
31
|
+
declarations = await this.#declarations?.();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
35
|
+
return new Map(list.map(key => [key, failure]));
|
|
36
|
+
}
|
|
37
|
+
let resolver;
|
|
38
|
+
return new Map(await Promise.all(list.map(async (key) => {
|
|
39
|
+
try {
|
|
40
|
+
const schema = await this.#read(key, () => resolver ??= NodePackageResolver.create(this.#projectRoot), declarations, true, document);
|
|
41
|
+
return [key, schema];
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return [key, error instanceof Error ? error : new Error(String(error))];
|
|
45
|
+
}
|
|
46
|
+
})));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Form schema of an installed plugin package that no instance declares yet (ADR 0053 §2 step 4):
|
|
50
|
+
* the package must be a production dependency (`dependencies` / `optionalDependencies`) of the
|
|
51
|
+
* project and a Plugin package. Returns the declared-instance form shape plus `declared: false`,
|
|
52
|
+
* `null` when such a package ships no `schema.json`, and `undefined` when `packageName` is not
|
|
53
|
+
* such a package (declared, not a dependency, not installed, not a Plugin, or not a package name).
|
|
54
|
+
*/
|
|
55
|
+
async readUndeclared(packageName, currentDocument) {
|
|
56
|
+
if (HOST_CONFIG_KEY_SET.has(packageName) || packageName === 'root'
|
|
57
|
+
|| !/^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/u.test(packageName))
|
|
58
|
+
return undefined;
|
|
59
|
+
let target;
|
|
60
|
+
try {
|
|
61
|
+
const packages = await NodePackageResolver.create(this.#projectRoot);
|
|
62
|
+
const root = await packages.root(this.#projectRoot);
|
|
63
|
+
const file = findConfigFile(this.#projectRoot);
|
|
64
|
+
const document = currentDocument ?? (file ? (await createConfigDocument(join(this.#projectRoot, file)).read()).document : {});
|
|
65
|
+
const declaration = readApplicationPluginDeclaration(document);
|
|
66
|
+
if (declaration.version !== 2)
|
|
67
|
+
return undefined;
|
|
68
|
+
if (declaration.instances.some(item => item.instanceKey === packageName || item.package === packageName))
|
|
69
|
+
return undefined;
|
|
70
|
+
const manifest = root.packageJson;
|
|
71
|
+
if (!Object.hasOwn(manifest.dependencies ?? {}, packageName)
|
|
72
|
+
&& !Object.hasOwn(manifest.optionalDependencies ?? {}, packageName))
|
|
73
|
+
return undefined;
|
|
74
|
+
try {
|
|
75
|
+
target = await resolveReference(packages, root, { package: packageName, instanceKey: packageName });
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// A dependency without a Zhin manifest is a plain library, not an undeclared plugin.
|
|
79
|
+
if (error instanceof ManifestValidationError)
|
|
80
|
+
return undefined;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
// A declaration through another reference (local path, alias) that resolves to the same
|
|
84
|
+
// package root already declares this package.
|
|
85
|
+
for (const item of declaration.instances) {
|
|
86
|
+
const declared = await resolveReference(packages, root, item).catch(() => undefined);
|
|
87
|
+
if (declared && target && declared.root === target.root)
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
throw new Error(`Cannot resolve schema for ${packageName}: invalid or unavailable Plugin manifest`, { cause: error });
|
|
93
|
+
}
|
|
94
|
+
if (!target)
|
|
95
|
+
return undefined;
|
|
96
|
+
const schema = (await this.#declarations?.())?.get(target.root) ?? await readOwnSchema(target.root, packageName, true);
|
|
97
|
+
if (!schema)
|
|
98
|
+
return null;
|
|
99
|
+
assertSchemaStructure(schema, `config declaration for ${packageName}`);
|
|
100
|
+
const form = jsonSchemaToConsoleSchema(schema);
|
|
101
|
+
return form ? Object.freeze({ ...form, declared: false }) : null;
|
|
102
|
+
}
|
|
103
|
+
async readAll(keys, document) {
|
|
104
|
+
let resolver;
|
|
105
|
+
const declarations = await this.#declarations?.();
|
|
106
|
+
if (!document) {
|
|
107
|
+
const file = findConfigFile(this.#projectRoot);
|
|
108
|
+
if (file)
|
|
109
|
+
document = (await createConfigDocument(join(this.#projectRoot, file)).read()).document;
|
|
110
|
+
}
|
|
111
|
+
const entries = await Promise.all([...keys].map(async (key) => [
|
|
112
|
+
key,
|
|
113
|
+
await this.#read(key, () => resolver ??= NodePackageResolver.create(this.#projectRoot), declarations, false, document),
|
|
114
|
+
]));
|
|
115
|
+
return Object.fromEntries(entries.filter((entry) => entry[1] != null));
|
|
116
|
+
}
|
|
117
|
+
async #read(pluginName, resolver, declarations, rawSchema = false, currentDocument) {
|
|
118
|
+
if (HOST_CONFIG_KEY_SET.has(pluginName)) {
|
|
119
|
+
// Host sections share the Runtime schema and its local definition scope.
|
|
120
|
+
// This is only a display projection; Runtime remains the config validator.
|
|
121
|
+
const raw = pluginName === 'plugin'
|
|
122
|
+
? declarations?.get(await realpath(this.#projectRoot)) ?? await readOwnSchema(this.#projectRoot, 'plugin', !rawSchema)
|
|
123
|
+
: HOST_CONFIG_SCHEMA.properties[pluginName];
|
|
124
|
+
if (rawSchema)
|
|
125
|
+
return raw && pluginName !== 'plugin'
|
|
126
|
+
? { ...raw, $defs: HOST_CONFIG_SCHEMA.$defs } : raw ?? null;
|
|
127
|
+
if (raw && pluginName === 'plugin')
|
|
128
|
+
assertSchemaStructure(raw, 'config declaration for plugin');
|
|
129
|
+
return jsonSchemaToConsoleSchema(raw && pluginName !== 'plugin' ? { ...raw, $defs: HOST_CONFIG_SCHEMA.$defs } : raw);
|
|
130
|
+
}
|
|
131
|
+
if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/u.test(pluginName)
|
|
132
|
+
&& !pluginName.split('/').every((key) => /^[a-z0-9][a-z0-9-]*$/u.test(key))) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
let target;
|
|
136
|
+
try {
|
|
137
|
+
const packages = await resolver();
|
|
138
|
+
const root = await packages.root(this.#projectRoot);
|
|
139
|
+
const file = findConfigFile(this.#projectRoot);
|
|
140
|
+
const document = currentDocument ?? (file ? (await createConfigDocument(join(this.#projectRoot, file)).read()).document : {});
|
|
141
|
+
const declaration = readApplicationPluginDeclaration(document);
|
|
142
|
+
if (declaration.version === 2) {
|
|
143
|
+
const name = pluginName.startsWith('root/') ? pluginName.slice(5) : pluginName;
|
|
144
|
+
const exact = declaration.instances.find(item => item.instanceKey === name);
|
|
145
|
+
const matches = exact ? [exact] : declaration.instances.filter(item => item.package === pluginName);
|
|
146
|
+
if (matches.length > 1)
|
|
147
|
+
throw new Error('此包存在多个插件实例,请使用精确 instanceKey');
|
|
148
|
+
if (matches[0])
|
|
149
|
+
target = await resolveReference(packages, root, matches[0]);
|
|
150
|
+
else if (pluginName === 'root')
|
|
151
|
+
target = root;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error instanceof Error && error.message.includes('多个插件实例'))
|
|
156
|
+
throw error;
|
|
157
|
+
throw new Error(`Cannot resolve schema for ${pluginName}: invalid or unavailable Plugin manifest`, { cause: error });
|
|
158
|
+
}
|
|
159
|
+
if (!target)
|
|
160
|
+
return null;
|
|
161
|
+
const schema = declarations?.get(target.root) ?? await readOwnSchema(target.root, pluginName, !rawSchema);
|
|
162
|
+
if (rawSchema)
|
|
163
|
+
return schema;
|
|
164
|
+
if (schema)
|
|
165
|
+
assertSchemaStructure(schema, `config declaration for ${pluginName}`);
|
|
166
|
+
return jsonSchemaToConsoleSchema(schema);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function resolveReference(resolver, owner, reference) {
|
|
170
|
+
try {
|
|
171
|
+
const pkg = await resolver.resolve(reference.package, owner);
|
|
172
|
+
return pkg.packageJson.zhin.type === 'plugin' ? pkg : undefined;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (error instanceof PackageResolutionError)
|
|
176
|
+
return undefined;
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Missing schemas have no form; malformed schemas must not masquerade as missing. */
|
|
181
|
+
async function readOwnSchema(root, name, forForm = true) {
|
|
182
|
+
let text;
|
|
183
|
+
try {
|
|
184
|
+
text = await readFile(join(root, 'schema.json'), 'utf8');
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
188
|
+
return null;
|
|
189
|
+
throw new Error(`Cannot read schema.json for ${name}`, { cause: error });
|
|
190
|
+
}
|
|
191
|
+
let value;
|
|
192
|
+
try {
|
|
193
|
+
value = JSON.parse(text);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
throw new Error(`Invalid JSON in schema.json for ${name}`);
|
|
197
|
+
}
|
|
198
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
199
|
+
throw new Error(`schema.json for ${name} must contain a JSON Schema object`);
|
|
200
|
+
}
|
|
201
|
+
const schema = value;
|
|
202
|
+
if (schema.type !== undefined && schema.type !== 'object') {
|
|
203
|
+
throw new Error(`schema.json for ${name} root schema type must be object`);
|
|
204
|
+
}
|
|
205
|
+
if (schema.properties !== undefined
|
|
206
|
+
&& (!schema.properties || typeof schema.properties !== 'object' || Array.isArray(schema.properties))) {
|
|
207
|
+
throw new Error(`schema.json for ${name} properties must be an object`);
|
|
208
|
+
}
|
|
209
|
+
if (forForm)
|
|
210
|
+
assertSchemaStructure(schema, `schema.json for ${name}`);
|
|
211
|
+
return schema;
|
|
212
|
+
}
|
|
213
|
+
/** Reject malformed child definitions before the form converter can silently omit them. */
|
|
214
|
+
function assertSchemaStructure(value, location) {
|
|
215
|
+
if (typeof value === 'boolean')
|
|
216
|
+
return;
|
|
217
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
218
|
+
throw new Error(`${location} must contain a supported schema object`);
|
|
219
|
+
}
|
|
220
|
+
const schema = value;
|
|
221
|
+
if (schema.$ref !== undefined && typeof schema.$ref !== 'string')
|
|
222
|
+
throw new Error(`${location}.$ref must be a string`);
|
|
223
|
+
if (schema.type !== undefined && !(typeof schema.type === 'string' || Array.isArray(schema.type) && schema.type.every(type => typeof type === 'string')))
|
|
224
|
+
throw new Error(`${location}.type must be a string or string array`);
|
|
225
|
+
if (schema.required !== undefined) {
|
|
226
|
+
const nativeFlag = typeof schema.required === 'boolean' && isConsoleSchemaJson(schema);
|
|
227
|
+
if (!nativeFlag && (!Array.isArray(schema.required)
|
|
228
|
+
|| !schema.required.every((key) => typeof key === 'string')
|
|
229
|
+
|| new Set(schema.required).size !== schema.required.length)) {
|
|
230
|
+
throw new Error(`${location}.required must be a unique string array or a native Schema boolean`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
for (const key of ['properties', '$defs', 'definitions', 'object', 'patternProperties', 'dependentSchemas']) {
|
|
234
|
+
const children = schema[key];
|
|
235
|
+
if (children === undefined)
|
|
236
|
+
continue;
|
|
237
|
+
if (!children || typeof children !== 'object' || Array.isArray(children)) {
|
|
238
|
+
throw new Error(`${location}.${key} must be an object`);
|
|
239
|
+
}
|
|
240
|
+
for (const [name, child] of Object.entries(children)) {
|
|
241
|
+
assertSchemaStructure(child, `${location}.${key}.${name}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const key of ['items', 'inner', 'additionalProperties', 'additional', 'additionalItems', 'contains', 'not', 'if', 'then', 'else']) {
|
|
245
|
+
const child = schema[key];
|
|
246
|
+
if (child === undefined || (key === 'additionalProperties' && typeof child === 'boolean'))
|
|
247
|
+
continue;
|
|
248
|
+
if (key === 'items' && Array.isArray(child)) {
|
|
249
|
+
child.forEach((item, index) => assertSchemaStructure(item, `${location}.items[${index}]`));
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
assertSchemaStructure(child, `${location}.${key}`);
|
|
253
|
+
}
|
|
254
|
+
for (const key of ['list', 'anyOf', 'oneOf', 'allOf', 'prefixItems']) {
|
|
255
|
+
const children = schema[key];
|
|
256
|
+
if (children === undefined)
|
|
257
|
+
continue;
|
|
258
|
+
if (!Array.isArray(children) || ['anyOf', 'oneOf', 'allOf'].includes(key) && children.length === 0)
|
|
259
|
+
throw new Error(`${location}.${key} must be a non-empty schema array`);
|
|
260
|
+
children.forEach((child, index) => assertSchemaStructure(child, `${location}.${key}[${index}]`));
|
|
261
|
+
}
|
|
262
|
+
}
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** `@zhin.js/gateway/config` — raw project configuration and `.env` access. */
|
|
2
|
+
export * from './config/ai-config-loader.js';
|
|
3
|
+
export * from './config/configuration.js';
|
|
4
|
+
export * from './config/configuration-chat-policy.js';
|
|
5
|
+
export * from './config/configuration-patch.js';
|
|
6
|
+
export * from './config/configuration-path.js';
|
|
7
|
+
export * from './config/configuration-projection.js';
|
|
8
|
+
export * from './config/configuration-redaction.js';
|
|
9
|
+
export * from './config/console-schema.js';
|
|
10
|
+
export * from './config/environment-files.js';
|
|
11
|
+
export * from './config/literal-secrets.js';
|
|
12
|
+
export * from './config/plugin-package-map.js';
|
|
13
|
+
export * from './config/plugin-schema-catalog.js';
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** `@zhin.js/gateway/config` — raw project configuration and `.env` access. */
|
|
2
|
+
export * from './config/ai-config-loader.js';
|
|
3
|
+
export * from './config/configuration.js';
|
|
4
|
+
export * from './config/configuration-chat-policy.js';
|
|
5
|
+
export * from './config/configuration-patch.js';
|
|
6
|
+
export * from './config/configuration-path.js';
|
|
7
|
+
export * from './config/configuration-projection.js';
|
|
8
|
+
export * from './config/configuration-redaction.js';
|
|
9
|
+
export * from './config/console-schema.js';
|
|
10
|
+
export * from './config/environment-files.js';
|
|
11
|
+
export * from './config/literal-secrets.js';
|
|
12
|
+
export * from './config/plugin-package-map.js';
|
|
13
|
+
export * from './config/plugin-schema-catalog.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type ProjectDatabaseConfig = {
|
|
2
|
+
readonly dialect: string;
|
|
3
|
+
} & Readonly<Record<string, unknown>>;
|
|
4
|
+
/** Project-relative default when no `database` block is configured. */
|
|
5
|
+
export declare const DEFAULT_PROJECT_DATABASE_FILE: string;
|
|
6
|
+
/** Validates an expanded `database` block (port of `normalizeDatabaseHostConfig`). */
|
|
7
|
+
export declare function normalizeProjectDatabaseConfig(input: ProjectDatabaseConfig): ProjectDatabaseConfig;
|
|
8
|
+
/**
|
|
9
|
+
* Resolves the expanded root config document's `database` block. A relative SQLite file is
|
|
10
|
+
* anchored at the project root (the instance runs with the project root as cwd, so both
|
|
11
|
+
* processes open the same file).
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveProjectDatabaseConfig(projectRoot: string, document: unknown): Promise<ProjectDatabaseConfig>;
|
|
14
|
+
/**
|
|
15
|
+
* The root config document with `${…}` expanded against process env and the project `.env`
|
|
16
|
+
* layers of `environment` (missing variables become empty, as in the instance). Only the gateway's
|
|
17
|
+
* own blocks are read from it (`database`, `console.terminal`): plugin declarations are not
|
|
18
|
+
* validated here, so a config the instance rejects still leaves the Console reachable.
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadExpandedProjectConfig(projectRoot: string, environment?: string): Promise<Readonly<Record<string, unknown>>>;
|
|
21
|
+
/** {@link resolveProjectDatabaseConfig} over {@link loadExpandedProjectConfig}. */
|
|
22
|
+
export declare function loadProjectDatabaseConfig(projectRoot: string, environment?: string): Promise<ProjectDatabaseConfig>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project `database` block as the gateway resolves it (ADR 0054 §7): the same file and
|
|
3
|
+
* driver settings as the instance's `resolveDatabaseConfig` (`@zhin.js/core`), ported here so the
|
|
4
|
+
* gateway never imports IM layers. Keep the two in step.
|
|
5
|
+
*/
|
|
6
|
+
import { mkdir } from 'node:fs/promises';
|
|
7
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
8
|
+
import { expandEnvironmentValue } from '@zhin.js/plugin-runtime';
|
|
9
|
+
import { createConfigDocument } from '@zhin.js/runtime';
|
|
10
|
+
import { findConfigFile } from '../project/config-file.js';
|
|
11
|
+
import { loadRuntimeEnvironmentLayers } from '../project/environment.js';
|
|
12
|
+
/** Project-relative default when no `database` block is configured. */
|
|
13
|
+
export const DEFAULT_PROJECT_DATABASE_FILE = join('.zhin', 'data.sqlite');
|
|
14
|
+
const SUPPORTED_DIALECTS = new Set(['memory', 'mongodb', 'mysql', 'pg', 'redis', 'sqlite']);
|
|
15
|
+
function configurationError(path, expectation, envExample) {
|
|
16
|
+
const envHint = envExample
|
|
17
|
+
? `若这里引用环境变量,请在项目根目录 .env 中设置 ${envExample}。`
|
|
18
|
+
: '若这里引用环境变量,请检查项目根目录 .env 中的对应变量。';
|
|
19
|
+
return new TypeError(`数据库配置 ${path} ${expectation}。${envHint}也可运行 \`zhin setup --database\` 重新配置。`);
|
|
20
|
+
}
|
|
21
|
+
function normalizePort(value, path, envExample) {
|
|
22
|
+
if (value === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
const normalized = typeof value === 'number'
|
|
25
|
+
? value
|
|
26
|
+
: typeof value === 'string' && /^\d+$/u.test(value.trim()) ? Number(value.trim()) : Number.NaN;
|
|
27
|
+
if (!Number.isInteger(normalized) || normalized < 1 || normalized > 65535) {
|
|
28
|
+
throw configurationError(path, '必须是 1-65535 的整数,当前值为空或无效', envExample);
|
|
29
|
+
}
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
function normalizeOptionalText(config, key, path, envExample) {
|
|
33
|
+
if (!(key in config))
|
|
34
|
+
return;
|
|
35
|
+
const value = config[key];
|
|
36
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
37
|
+
throw configurationError(path, '不能为空', envExample);
|
|
38
|
+
config[key] = value.trim();
|
|
39
|
+
}
|
|
40
|
+
function normalizeRequiredText(config, key, path, envExample) {
|
|
41
|
+
if (!(key in config))
|
|
42
|
+
config[key] = undefined;
|
|
43
|
+
normalizeOptionalText(config, key, path, envExample);
|
|
44
|
+
}
|
|
45
|
+
function normalizeRedisDatabase(value) {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
const normalized = typeof value === 'number'
|
|
49
|
+
? value
|
|
50
|
+
: typeof value === 'string' && /^\d+$/u.test(value.trim()) ? Number(value.trim()) : Number.NaN;
|
|
51
|
+
if (!Number.isInteger(normalized) || normalized < 0) {
|
|
52
|
+
throw configurationError('database.database', '必须是大于或等于 0 的整数', 'REDIS_DB=0');
|
|
53
|
+
}
|
|
54
|
+
return normalized;
|
|
55
|
+
}
|
|
56
|
+
/** Validates an expanded `database` block (port of `normalizeDatabaseHostConfig`). */
|
|
57
|
+
export function normalizeProjectDatabaseConfig(input) {
|
|
58
|
+
const dialect = input.dialect;
|
|
59
|
+
if (!SUPPORTED_DIALECTS.has(dialect)) {
|
|
60
|
+
throw new TypeError(`数据库配置 database.dialect 不支持 ${JSON.stringify(dialect)};可选值为 sqlite、mysql、pg、mongodb、redis、memory。`);
|
|
61
|
+
}
|
|
62
|
+
const config = { ...input, dialect };
|
|
63
|
+
switch (dialect) {
|
|
64
|
+
case 'mysql':
|
|
65
|
+
normalizeOptionalText(config, 'host', 'database.host', 'DB_HOST=127.0.0.1');
|
|
66
|
+
normalizeOptionalText(config, 'user', 'database.user', 'DB_USER=root');
|
|
67
|
+
normalizeOptionalText(config, 'database', 'database.database', 'DB_DATABASE=zhin_bot');
|
|
68
|
+
normalizeOptionalText(config, 'uri', 'database.uri');
|
|
69
|
+
config.port = normalizePort(config.port, 'database.port', 'DB_PORT=3306');
|
|
70
|
+
if (config.port === undefined)
|
|
71
|
+
delete config.port;
|
|
72
|
+
break;
|
|
73
|
+
case 'pg':
|
|
74
|
+
normalizeOptionalText(config, 'host', 'database.host', 'DB_HOST=127.0.0.1');
|
|
75
|
+
normalizeOptionalText(config, 'user', 'database.user', 'DB_USER=postgres');
|
|
76
|
+
normalizeOptionalText(config, 'database', 'database.database', 'DB_DATABASE=zhin_bot');
|
|
77
|
+
normalizeOptionalText(config, 'connectionString', 'database.connectionString');
|
|
78
|
+
config.port = normalizePort(config.port, 'database.port', 'DB_PORT=5432');
|
|
79
|
+
if (config.port === undefined)
|
|
80
|
+
delete config.port;
|
|
81
|
+
break;
|
|
82
|
+
case 'mongodb':
|
|
83
|
+
normalizeRequiredText(config, 'url', 'database.url', 'DB_URL=mongodb://127.0.0.1:27017');
|
|
84
|
+
normalizeRequiredText(config, 'dbName', 'database.dbName', 'DB_NAME=zhin_bot');
|
|
85
|
+
if (!/^mongodb(?:\+srv)?:\/\//u.test(String(config.url)))
|
|
86
|
+
throw configurationError('database.url', '必须使用 mongodb:// 或 mongodb+srv://');
|
|
87
|
+
break;
|
|
88
|
+
case 'redis': {
|
|
89
|
+
normalizeOptionalText(config, 'url', 'database.url', 'REDIS_URL=redis://127.0.0.1:6379');
|
|
90
|
+
if (config.socket !== undefined) {
|
|
91
|
+
if (!config.socket || typeof config.socket !== 'object' || Array.isArray(config.socket))
|
|
92
|
+
throw configurationError('database.socket', '必须是对象');
|
|
93
|
+
const socket = { ...config.socket };
|
|
94
|
+
normalizeOptionalText(socket, 'host', 'database.socket.host', 'REDIS_HOST=127.0.0.1');
|
|
95
|
+
socket.port = normalizePort(socket.port, 'database.socket.port', 'REDIS_PORT=6379');
|
|
96
|
+
if (socket.port === undefined)
|
|
97
|
+
delete socket.port;
|
|
98
|
+
config.socket = Object.freeze(socket);
|
|
99
|
+
}
|
|
100
|
+
config.database = normalizeRedisDatabase(config.database);
|
|
101
|
+
if (config.database === undefined)
|
|
102
|
+
delete config.database;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case 'sqlite':
|
|
106
|
+
if (config.filename === undefined)
|
|
107
|
+
config.filename = './data/bot.db';
|
|
108
|
+
normalizeOptionalText(config, 'filename', 'database.filename');
|
|
109
|
+
break;
|
|
110
|
+
case 'memory':
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
return Object.freeze(config);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolves the expanded root config document's `database` block. A relative SQLite file is
|
|
117
|
+
* anchored at the project root (the instance runs with the project root as cwd, so both
|
|
118
|
+
* processes open the same file).
|
|
119
|
+
*/
|
|
120
|
+
export async function resolveProjectDatabaseConfig(projectRoot, document) {
|
|
121
|
+
const configured = document && typeof document === 'object' ? document.database : undefined;
|
|
122
|
+
if (configured && typeof configured === 'object' && !Array.isArray(configured)) {
|
|
123
|
+
const value = configured;
|
|
124
|
+
const normalized = normalizeProjectDatabaseConfig({ ...value, dialect: typeof value.dialect === 'string' ? value.dialect : 'sqlite' });
|
|
125
|
+
if (normalized.dialect === 'sqlite' && typeof normalized.filename === 'string' && !isSqliteMemory(normalized.filename) && !isAbsolute(normalized.filename)) {
|
|
126
|
+
return Object.freeze({ ...normalized, filename: resolve(projectRoot, normalized.filename) });
|
|
127
|
+
}
|
|
128
|
+
return normalized;
|
|
129
|
+
}
|
|
130
|
+
const filename = join(projectRoot, DEFAULT_PROJECT_DATABASE_FILE);
|
|
131
|
+
await mkdir(dirname(filename), { recursive: true });
|
|
132
|
+
return Object.freeze({ dialect: 'sqlite', filename });
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The root config document with `${…}` expanded against process env and the project `.env`
|
|
136
|
+
* layers of `environment` (missing variables become empty, as in the instance). Only the gateway's
|
|
137
|
+
* own blocks are read from it (`database`, `console.terminal`): plugin declarations are not
|
|
138
|
+
* validated here, so a config the instance rejects still leaves the Console reachable.
|
|
139
|
+
*/
|
|
140
|
+
export async function loadExpandedProjectConfig(projectRoot, environment = 'development') {
|
|
141
|
+
const file = findConfigFile(projectRoot);
|
|
142
|
+
if (!file)
|
|
143
|
+
return Object.freeze({});
|
|
144
|
+
const [document, layers] = await Promise.all([
|
|
145
|
+
createConfigDocument(join(projectRoot, file)).read().then((snapshot) => snapshot.document),
|
|
146
|
+
loadRuntimeEnvironmentLayers(projectRoot, environment),
|
|
147
|
+
]);
|
|
148
|
+
if (!document || typeof document !== 'object' || Array.isArray(document))
|
|
149
|
+
return Object.freeze({});
|
|
150
|
+
const overlay = layers.environments?.[environment];
|
|
151
|
+
return expandEnvironmentValue(document, (key) => overlay?.[key] ?? layers.base?.[key] ?? process.env[key]);
|
|
152
|
+
}
|
|
153
|
+
/** {@link resolveProjectDatabaseConfig} over {@link loadExpandedProjectConfig}. */
|
|
154
|
+
export async function loadProjectDatabaseConfig(projectRoot, environment = 'development') {
|
|
155
|
+
return resolveProjectDatabaseConfig(projectRoot, await loadExpandedProjectConfig(projectRoot, environment));
|
|
156
|
+
}
|
|
157
|
+
function isSqliteMemory(filename) {
|
|
158
|
+
return filename === ':memory:' || filename.startsWith('file::memory:');
|
|
159
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { DatabaseHostModel } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import type { ProjectDatabaseConfig } from './config.js';
|
|
3
|
+
/**
|
|
4
|
+
* How long a gateway statement retries `SQLITE_BUSY` while the instance holds the write lock
|
|
5
|
+
* (`busyRetryMs` in `@zhin.js/database`). The retry backs off asynchronously, so the gateway's
|
|
6
|
+
* event loop keeps serving requests during the wait; a synchronous `busy_timeout` would stall
|
|
7
|
+
* every request for as long as an instance transaction runs. Past the budget the request
|
|
8
|
+
* answers `503 database_busy` ({@link isDatabaseBusyError}).
|
|
9
|
+
*/
|
|
10
|
+
export declare const GATEWAY_SQLITE_BUSY_RETRY_MS = 5000;
|
|
11
|
+
/** Seconds a `503 database_busy` asks the client to wait. */
|
|
12
|
+
export declare const DATABASE_BUSY_RETRY_AFTER_SECONDS = 2;
|
|
13
|
+
/** The database stayed locked by another connection for the whole retry budget. */
|
|
14
|
+
export declare function isDatabaseBusyError(error: unknown): boolean;
|
|
15
|
+
/** `503 { code: 'database_busy' }` with `Retry-After`. */
|
|
16
|
+
export declare function databaseBusyResponse(extra?: Readonly<Record<string, unknown>>): Response;
|
|
17
|
+
/** One database connection; tables are defined before {@link start}. */
|
|
18
|
+
export declare class ProjectDatabase {
|
|
19
|
+
#private;
|
|
20
|
+
readonly config: ProjectDatabaseConfig;
|
|
21
|
+
constructor(config: ProjectDatabaseConfig);
|
|
22
|
+
get dialect(): string;
|
|
23
|
+
get started(): boolean;
|
|
24
|
+
define(name: string, definition: Record<string, unknown>): void;
|
|
25
|
+
tables(): readonly string[];
|
|
26
|
+
readonly models: Readonly<{
|
|
27
|
+
get: (name: string) => DatabaseHostModel | undefined;
|
|
28
|
+
}>;
|
|
29
|
+
start(): Promise<void>;
|
|
30
|
+
stop(): Promise<void>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gateway's own connection to the project database (ADR 0054 §7). It defines and serves only
|
|
3
|
+
* the tables the gateway owns (`console_*`, ADR 0049); the instance opens the same database for
|
|
4
|
+
* everything else. SQLite runs in WAL mode (`@zhin.js/database`), so both processes can hold the
|
|
5
|
+
* file open.
|
|
6
|
+
*/
|
|
7
|
+
import { isSqliteBusyError, Registry } from '@zhin.js/database';
|
|
8
|
+
/**
|
|
9
|
+
* How long a gateway statement retries `SQLITE_BUSY` while the instance holds the write lock
|
|
10
|
+
* (`busyRetryMs` in `@zhin.js/database`). The retry backs off asynchronously, so the gateway's
|
|
11
|
+
* event loop keeps serving requests during the wait; a synchronous `busy_timeout` would stall
|
|
12
|
+
* every request for as long as an instance transaction runs. Past the budget the request
|
|
13
|
+
* answers `503 database_busy` ({@link isDatabaseBusyError}).
|
|
14
|
+
*/
|
|
15
|
+
export const GATEWAY_SQLITE_BUSY_RETRY_MS = 5_000;
|
|
16
|
+
/** Seconds a `503 database_busy` asks the client to wait. */
|
|
17
|
+
export const DATABASE_BUSY_RETRY_AFTER_SECONDS = 2;
|
|
18
|
+
/** The database stayed locked by another connection for the whole retry budget. */
|
|
19
|
+
export function isDatabaseBusyError(error) {
|
|
20
|
+
for (let current = error, depth = 0; current && depth < 4; depth += 1) {
|
|
21
|
+
if (isSqliteBusyError(current))
|
|
22
|
+
return true;
|
|
23
|
+
current = current.cause;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
/** `503 { code: 'database_busy' }` with `Retry-After`. */
|
|
28
|
+
export function databaseBusyResponse(extra = {}) {
|
|
29
|
+
return new Response(JSON.stringify({
|
|
30
|
+
success: false,
|
|
31
|
+
code: 'database_busy',
|
|
32
|
+
error: '数据库正忙,请稍后重试',
|
|
33
|
+
...extra,
|
|
34
|
+
}), {
|
|
35
|
+
status: 503,
|
|
36
|
+
headers: {
|
|
37
|
+
'content-type': 'application/json; charset=utf-8',
|
|
38
|
+
'cache-control': 'no-store',
|
|
39
|
+
'retry-after': String(DATABASE_BUSY_RETRY_AFTER_SECONDS),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function wrapModel(model) {
|
|
44
|
+
return Object.freeze({
|
|
45
|
+
select: (...fields) => {
|
|
46
|
+
if (fields.includes('*'))
|
|
47
|
+
throw new TypeError('select() requires explicit column names');
|
|
48
|
+
const selection = model.select(...fields);
|
|
49
|
+
const chain = {
|
|
50
|
+
where: (query) => { selection.where(query); return chain; },
|
|
51
|
+
orderBy: (field, direction) => { selection.orderBy?.(field, direction); return chain; },
|
|
52
|
+
limit: (count) => { selection.limit?.(count); return chain; },
|
|
53
|
+
then: (onfulfilled, onrejected) => Promise.resolve(selection).then(onfulfilled, onrejected),
|
|
54
|
+
};
|
|
55
|
+
return chain;
|
|
56
|
+
},
|
|
57
|
+
insert: (row) => Promise.resolve(model.insert(row)),
|
|
58
|
+
delete: () => ({ where: (query) => Promise.resolve(model.delete(query)) }),
|
|
59
|
+
update: (patch) => ({ where: (query) => Promise.resolve(model.update(patch).where(query)) }),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** One database connection; tables are defined before {@link start}. */
|
|
63
|
+
export class ProjectDatabase {
|
|
64
|
+
config;
|
|
65
|
+
#db;
|
|
66
|
+
#tables = new Set();
|
|
67
|
+
#wrapped = new Map();
|
|
68
|
+
#started = false;
|
|
69
|
+
constructor(config) {
|
|
70
|
+
this.config = config;
|
|
71
|
+
const { dialect, ...connection } = config;
|
|
72
|
+
// No synchronous busy_timeout: it would block the gateway's event loop (ADR 0054 §7).
|
|
73
|
+
const options = dialect === 'sqlite' && connection.busyRetryMs === undefined
|
|
74
|
+
? { ...connection, busyRetryMs: GATEWAY_SQLITE_BUSY_RETRY_MS }
|
|
75
|
+
: connection;
|
|
76
|
+
this.#db = Registry.create(dialect, options);
|
|
77
|
+
}
|
|
78
|
+
get dialect() {
|
|
79
|
+
return this.config.dialect;
|
|
80
|
+
}
|
|
81
|
+
get started() {
|
|
82
|
+
return this.#started;
|
|
83
|
+
}
|
|
84
|
+
define(name, definition) {
|
|
85
|
+
if (this.#started)
|
|
86
|
+
throw new Error(`Project database already started; cannot define table ${name}`);
|
|
87
|
+
if (this.#tables.has(name))
|
|
88
|
+
return;
|
|
89
|
+
this.#tables.add(name);
|
|
90
|
+
this.#db.define(name, definition);
|
|
91
|
+
}
|
|
92
|
+
tables() {
|
|
93
|
+
return [...this.#tables];
|
|
94
|
+
}
|
|
95
|
+
models = Object.freeze({
|
|
96
|
+
get: (name) => {
|
|
97
|
+
if (!this.#started)
|
|
98
|
+
return undefined;
|
|
99
|
+
const cached = this.#wrapped.get(name);
|
|
100
|
+
if (cached)
|
|
101
|
+
return cached;
|
|
102
|
+
const raw = this.#db.models.get(name);
|
|
103
|
+
if (!raw)
|
|
104
|
+
return undefined;
|
|
105
|
+
const model = wrapModel(raw);
|
|
106
|
+
this.#wrapped.set(name, model);
|
|
107
|
+
return model;
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
async start() {
|
|
111
|
+
if (this.#started)
|
|
112
|
+
return;
|
|
113
|
+
try {
|
|
114
|
+
await this.#db.start();
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
await this.#db.stop().catch(() => undefined);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
this.#started = true;
|
|
121
|
+
this.#wrapped.clear();
|
|
122
|
+
}
|
|
123
|
+
async stop() {
|
|
124
|
+
if (!this.#started)
|
|
125
|
+
return;
|
|
126
|
+
this.#started = false;
|
|
127
|
+
this.#wrapped.clear();
|
|
128
|
+
await this.#db.stop();
|
|
129
|
+
}
|
|
130
|
+
}
|
package/lib/database.js
ADDED