@feltdb/core 0.4.3 → 0.4.5
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 +13 -0
- package/dist/migrate/analyzer/index.d.ts +2 -0
- package/dist/migrate/analyzer/index.js +34 -0
- package/dist/migrate/benchmark/index.d.ts +13 -0
- package/dist/migrate/benchmark/index.js +8 -0
- package/dist/migrate/capability-gate/index.d.ts +3 -0
- package/dist/migrate/capability-gate/index.js +3 -0
- package/dist/migrate/classifier/index.d.ts +3 -0
- package/dist/migrate/classifier/index.js +53 -0
- package/dist/migrate/cli/index.d.ts +2 -0
- package/dist/migrate/cli/index.js +201 -0
- package/dist/migrate/domain-model/index.d.ts +2 -0
- package/dist/migrate/domain-model/index.js +4 -0
- package/dist/migrate/generator/index.d.ts +2 -0
- package/dist/migrate/generator/index.js +21 -0
- package/dist/migrate/index.d.ts +16 -0
- package/dist/migrate/index.js +16 -0
- package/dist/migrate/measurement/index.d.ts +7 -0
- package/dist/migrate/measurement/index.js +1 -0
- package/dist/migrate/migrator/index.d.ts +2 -0
- package/dist/migrate/migrator/index.js +35 -0
- package/dist/migrate/planner/index.d.ts +2 -0
- package/dist/migrate/planner/index.js +12 -0
- package/dist/migrate/prover/index.d.ts +9 -0
- package/dist/migrate/prover/index.js +3 -0
- package/dist/migrate/reporter/index.d.ts +4 -0
- package/dist/migrate/reporter/index.js +1 -0
- package/dist/migrate/runtime-dependencies/index.d.ts +13 -0
- package/dist/migrate/runtime-dependencies/index.js +112 -0
- package/dist/migrate/state-contract/index.d.ts +13 -0
- package/dist/migrate/state-contract/index.js +49 -0
- package/dist/migrate/types/index.d.ts +228 -0
- package/dist/migrate/types/index.js +1 -0
- package/dist/migrate/ui-wiring/index.d.ts +18 -0
- package/dist/migrate/ui-wiring/index.js +2 -0
- package/dist/migrate/util.d.ts +5 -0
- package/dist/migrate/util.js +10 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/react/useCapability.d.ts +11 -0
- package/dist/react/useCapability.d.ts.map +1 -0
- package/dist/react/useCapability.js +30 -0
- package/dist/react/useCollection.d.ts +17 -0
- package/dist/react/useCollection.d.ts.map +1 -0
- package/dist/react/useCollection.js +69 -0
- package/dist/react/useFeltDB.d.ts +136 -0
- package/dist/react/useFeltDB.d.ts.map +1 -0
- package/dist/react/useFeltDB.js +350 -0
- package/dist/studio/.gitkeep +1 -0
- package/dist/studio/KeyManagementPanel-BOvWTRyH.js +246 -0
- package/dist/studio/app.d.ts +10 -0
- package/dist/studio/app.d.ts.map +1 -0
- package/dist/studio/components/AgentExplorer.d.ts +9 -0
- package/dist/studio/components/AgentExplorer.d.ts.map +1 -0
- package/dist/studio/components/ApplicationDesigner.d.ts +11 -0
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -0
- package/dist/studio/components/CapabilityExplorer.d.ts +9 -0
- package/dist/studio/components/CapabilityExplorer.d.ts.map +1 -0
- package/dist/studio/components/ConflictExplorer.d.ts +9 -0
- package/dist/studio/components/ConflictExplorer.d.ts.map +1 -0
- package/dist/studio/components/ExecutionViewer.d.ts +9 -0
- package/dist/studio/components/ExecutionViewer.d.ts.map +1 -0
- package/dist/studio/components/GlobalSearch.d.ts +10 -0
- package/dist/studio/components/GlobalSearch.d.ts.map +1 -0
- package/dist/studio/components/HealthCenter.d.ts +9 -0
- package/dist/studio/components/HealthCenter.d.ts.map +1 -0
- package/dist/studio/components/KeyManagementPanel.d.ts +9 -0
- package/dist/studio/components/KeyManagementPanel.d.ts.map +1 -0
- package/dist/studio/components/KeyManagementPanel.js +2 -0
- package/dist/studio/components/ManagedInstancePanel.d.ts +4 -0
- package/dist/studio/components/ManagedInstancePanel.d.ts.map +1 -0
- package/dist/studio/components/ManagedInstancePanel.js +281 -0
- package/dist/studio/components/OperationsExplorer.d.ts +10 -0
- package/dist/studio/components/OperationsExplorer.d.ts.map +1 -0
- package/dist/studio/components/OverviewDashboard.d.ts +8 -0
- package/dist/studio/components/OverviewDashboard.d.ts.map +1 -0
- package/dist/studio/components/PeerMap.d.ts +8 -0
- package/dist/studio/components/PeerMap.d.ts.map +1 -0
- package/dist/studio/components/ProvenanceViewer.d.ts +11 -0
- package/dist/studio/components/ProvenanceViewer.d.ts.map +1 -0
- package/dist/studio/components/ReferenceExplorer.d.ts +10 -0
- package/dist/studio/components/ReferenceExplorer.d.ts.map +1 -0
- package/dist/studio/components/SettingsPanel.d.ts +7 -0
- package/dist/studio/components/SettingsPanel.d.ts.map +1 -0
- package/dist/studio/components/StateExplorer.d.ts +10 -0
- package/dist/studio/components/StateExplorer.d.ts.map +1 -0
- package/dist/studio/components/WorkflowVisualizer.d.ts +9 -0
- package/dist/studio/components/WorkflowVisualizer.d.ts.map +1 -0
- package/dist/studio/components/index.d.ts +24 -0
- package/dist/studio/components/index.d.ts.map +1 -0
- package/dist/studio/components/index.js +4 -0
- package/dist/studio/components-BRYUceo9.js +1711 -0
- package/dist/studio/hooks/index.d.ts +37 -0
- package/dist/studio/hooks/index.d.ts.map +1 -0
- package/dist/studio/index.d.ts +27 -0
- package/dist/studio/index.d.ts.map +1 -0
- package/dist/studio/index.html +30 -0
- package/dist/studio/index.js +236 -0
- package/dist/studio/main.d.ts +1 -0
- package/dist/studio/main.d.ts.map +1 -0
- package/dist/studio/studio.css +2 -0
- package/dist/studio/types/index.d.ts +133 -0
- package/dist/studio/types/index.d.ts.map +1 -0
- package/dist/studio/utils/api.d.ts +27 -0
- package/dist/studio/utils/api.d.ts.map +1 -0
- package/dist/studio/utils/format.d.ts +24 -0
- package/dist/studio/utils/format.d.ts.map +1 -0
- package/dist/studio/utils/index.d.ts +7 -0
- package/dist/studio/utils/index.d.ts.map +1 -0
- package/dist/studio/utils/index.js +75 -0
- package/dist/studio/utils/managed-instance.d.ts +40 -0
- package/dist/studio/utils/managed-instance.d.ts.map +1 -0
- package/dist/studio/utils/managed-instance.js +51 -0
- package/dist/studio-app/.gitkeep +1 -0
- package/dist/studio-app/assets/feltdb_wasm-BXMn9UxO.js +1 -0
- package/dist/studio-app/assets/feltdb_wasm_bg-bYYbZeRM.wasm +0 -0
- package/dist/studio-app/assets/index-B-lZjdtI.js +28 -0
- package/dist/studio-app/assets/index-CB-Eed9V.css +1 -0
- package/dist/studio-app/index.html +3 -0
- package/package.json +33 -4
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { basename, join, relative } from 'node:path';
|
|
3
|
+
import { hash } from '../util.js';
|
|
4
|
+
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'target', '.feltdb', 'coverage']);
|
|
5
|
+
const ENV = /^(?:export\s+)?(POSTGRES_(?:HOST|PORT|DB|USER|PASSWORD)|PG(?:HOST|PORT|DATABASE|USER|PASSWORD)|DATABASE_(?:URL|HOST|PORT|NAME|USER|PASSWORD))\s*[:=]/i;
|
|
6
|
+
const CLIENT = /\b(?:@neondatabase\/serverless|node-postgres|tokio-postgres|postgres\.js|pglite|@electric-sql\/pglite|sqlx|diesel|knex|typeorm|prisma|drizzle-orm|require\(['"]pg['"]\)|from\s+['"]pg['"])/i;
|
|
7
|
+
const CONNECTION = /\b(?:new\s+(?:Pool|Client)\s*\(|createPool\s*\(|postgres\s*\(|connect\s*\(.*postgres)/i;
|
|
8
|
+
const SQL = /\b(?:SELECT\s+.+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM|JOIN\s+\w+|CREATE\s+(?:TABLE|INDEX)|ALTER\s+TABLE)\b/i;
|
|
9
|
+
const STARTUP = /(?:wait-for-postgres|dockerize\s+-wait\s+tcp:\/\/(?:postgres|[^\s:]+):5432|pg_isready|psql\s)/i;
|
|
10
|
+
const HEALTH = /(?:pg_isready|postgres(?:ql)?[^\n]{0,30}health|health[^\n]{0,30}(?:postgres|5432))/i;
|
|
11
|
+
const IMAGE = /(?:image\s*:\s*postgres(?:ql)?(?::|\s|$)|container.*postgres|services?\s*:\s*postgres)/i;
|
|
12
|
+
const DEPLOY = /(?:depends_on[^\n]*postgres|postgres\s*:\s*(?:condition|required)|(?:host|hostname)\s*:\s*postgres|tcp:\/\/postgres:5432)/i;
|
|
13
|
+
const TEXT = /(?:^|\/)(?:Dockerfile[^/]*|[^/]*\.(?:[cm]?[jt]sx?|py|rb|go|rs|sql|ya?ml|toml|json|env|example|md|sh)|\.env[^/]*)$/i;
|
|
14
|
+
async function walk(root, dir = root, out = []) { for (const n of await readdir(dir)) {
|
|
15
|
+
if (SKIP.has(n))
|
|
16
|
+
continue;
|
|
17
|
+
const p = join(dir, n), s = await stat(p);
|
|
18
|
+
if (s.isDirectory())
|
|
19
|
+
await walk(root, p, out);
|
|
20
|
+
else if (TEXT.test(p))
|
|
21
|
+
out.push(p);
|
|
22
|
+
} return out.sort(); }
|
|
23
|
+
function context(file, line) { const p = file.toLowerCase(); if (/(?:^|\/)(?:migrations?|migration-sherpa|feltdb-migration)(?:\/|\.|$)/.test(p))
|
|
24
|
+
return 'MIGRATION_SOURCE'; if (/(?:^|\/)(?:rollback|backout)(?:\/|\.|$)/.test(p) || /rollback[-_ ]only/i.test(line))
|
|
25
|
+
return 'ROLLBACK_ONLY'; if (/(?:^|\/)(?:legacy|fallback)(?:\/|\.|$)/.test(p) || /legacy[-_ ]fallback/i.test(line))
|
|
26
|
+
return 'LEGACY_FALLBACK'; if (/(?:^|\/)(?:test|tests|e2e|fixtures?|__tests__)(?:\/|\.|$)|(?:\.|_)(?:test|tests|spec|acceptance_tests)\./.test(p))
|
|
27
|
+
return 'TEST_ONLY'; if (/(?:^|\/)(?:dev|development|examples?)(?:\/|\.|$)|compose\.dev/.test(p))
|
|
28
|
+
return 'DEVELOPMENT_ONLY'; if (/(?:^|\/)(?:docs?|ops)(?:\/|$)|readme|\.md$|\.github\/workflows\/build|(?:baseline|results|report|metrics)[^/]*\.json$|(?:^|\/)persistence_[^/]*\.json$/.test(p))
|
|
29
|
+
return 'BUILD_ONLY'; return 'RUNTIME'; }
|
|
30
|
+
function redact(line) { return line.replace(/^((?:export\s+)?(?:POSTGRES_|PG|DATABASE_)[A-Z_]+\s*[:=]\s*).*/i, '$1<redacted>').trim().slice(0, 300); }
|
|
31
|
+
function action(code) { return code === 'POSTGRES_RUNTIME_ENV' ? 'Remove or replace with the configured FeltDB runtime contract.' : code === 'POSTGRES_RUNTIME_HEALTHCHECK' ? 'Replace with a FeltDB health/readiness check.' : 'Remove from the application runtime or explicitly classify it as migration, rollback, test, development, build, or fallback infrastructure.'; }
|
|
32
|
+
export async function scanPostgresDependencies(sourceRoot, feltDbRuntime, overrides = {}) {
|
|
33
|
+
const findings = [];
|
|
34
|
+
for (const absolute of await walk(sourceRoot)) {
|
|
35
|
+
const file = relative(sourceRoot, absolute), content = await readFile(absolute, 'utf8');
|
|
36
|
+
const isManifest = basename(file) === 'package.json', isDeployment = /(?:docker|compose|kubernetes|k8s|helm|deploy|\.github\/workflows|fly\.toml)/i.test(file);
|
|
37
|
+
let packageJson = {};
|
|
38
|
+
if (isManifest)
|
|
39
|
+
try {
|
|
40
|
+
packageJson = JSON.parse(content);
|
|
41
|
+
}
|
|
42
|
+
catch { /* scan malformed JSON as text */ }
|
|
43
|
+
for (const [i, line] of content.split(/\r?\n/).entries()) {
|
|
44
|
+
const matches = [];
|
|
45
|
+
const env = line.match(ENV), comment = /^\s*(?:\/\/|#(?!\!)|\/\*|\*)/.test(line);
|
|
46
|
+
if (env) {
|
|
47
|
+
const generic = env[1].toUpperCase().startsWith('DATABASE_');
|
|
48
|
+
const postgresValue = /(?:postgres(?:ql)?:\/\/|\bpostgres\b|\b5432\b)/i.test(line);
|
|
49
|
+
if (!generic || postgresValue || CLIENT.test(content))
|
|
50
|
+
matches.push([postgresValue ? 'POSTGRES_RUNTIME_URL' : 'POSTGRES_RUNTIME_ENV', 'environment']);
|
|
51
|
+
}
|
|
52
|
+
if (CLIENT.test(line) && !comment)
|
|
53
|
+
matches.push(['POSTGRES_RUNTIME_CLIENT', 'connection']);
|
|
54
|
+
if (CONNECTION.test(line) && !comment && CLIENT.test(content))
|
|
55
|
+
matches.push(['POSTGRES_RUNTIME_CONNECTION', 'connection']);
|
|
56
|
+
if (SQL.test(line) && !comment && (/\.sql$/i.test(file) || CLIENT.test(content) || /(?:query|execute|raw)\s*\(/i.test(line)))
|
|
57
|
+
matches.push(['POSTGRES_RUNTIME_SQL', 'sql']);
|
|
58
|
+
if (STARTUP.test(line) && !comment)
|
|
59
|
+
matches.push(['POSTGRES_RUNTIME_STARTUP_DEPENDENCY', 'startup']);
|
|
60
|
+
if (HEALTH.test(line) && !comment)
|
|
61
|
+
matches.push(['POSTGRES_RUNTIME_HEALTHCHECK', 'healthcheck']);
|
|
62
|
+
if ((IMAGE.test(line) || (isDeployment && /^\s+postgres\s*:\s*$/.test(line))) && !comment)
|
|
63
|
+
matches.push(['POSTGRES_RUNTIME_CONTAINER', 'container']);
|
|
64
|
+
if ((DEPLOY.test(line) || (isDeployment && /^\s*-\s*postgres\s*$/.test(line))) && !comment)
|
|
65
|
+
matches.push(['POSTGRES_RUNTIME_DEPLOYMENT_DEPENDENCY', 'deployment']);
|
|
66
|
+
for (const [code, kind] of matches) {
|
|
67
|
+
const key = `${file}:${i + 1}`, base = context(file, line);
|
|
68
|
+
let classification = overrides[key] ?? overrides[file] ?? base;
|
|
69
|
+
if (/pglite/i.test(line) && classification === 'RUNTIME' && /(legacy|fallback)/i.test(content))
|
|
70
|
+
classification = 'LEGACY_FALLBACK';
|
|
71
|
+
const runtime = classification === 'RUNTIME';
|
|
72
|
+
findings.push({ id: hash([file, i + 1, code, line]).slice(0, 16), code, file, line: i + 1, classification, runtime, kind, evidence: redact(line), action: action(code) });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (isManifest)
|
|
76
|
+
for (const section of ['dependencies', 'devDependencies'])
|
|
77
|
+
for (const [name] of Object.entries(packageJson[section] ?? {})) {
|
|
78
|
+
if (!CLIENT.test(name))
|
|
79
|
+
continue;
|
|
80
|
+
const key = `${file}:${section}:${name}`, classification = overrides[key] ?? overrides[file] ?? (section === 'devDependencies' ? 'BUILD_ONLY' : context(file, name));
|
|
81
|
+
findings.push({ id: hash(key).slice(0, 16), code: 'POSTGRES_RUNTIME_CLIENT', file, line: 1, classification, runtime: classification === 'RUNTIME', kind: 'dependency', evidence: `${section}.${name}`, action: action('POSTGRES_RUNTIME_CLIENT') });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const unique = [...new Map(findings.map(f => [`${f.file}:${f.line}:${f.code}:${f.evidence}`, f])).values()].sort((a, b) => `${a.file}:${a.line}:${a.code}`.localeCompare(`${b.file}:${b.line}:${b.code}`));
|
|
85
|
+
const group = (c) => unique.filter(f => f.classification === c);
|
|
86
|
+
const core = { version: 1, generatedAt: new Date().toISOString(), sourceRoot, findings: unique, runtime: group('RUNTIME'), migrationSource: group('MIGRATION_SOURCE'), legacyFallback: group('LEGACY_FALLBACK'), testOnly: group('TEST_ONLY'), developmentOnly: group('DEVELOPMENT_ONLY'), buildOnly: group('BUILD_ONLY'), rollbackOnly: group('ROLLBACK_ONLY'), feltDbRuntime };
|
|
87
|
+
return { ...core, runtimeDependencyHash: hash({ ...core, generatedAt: undefined }) };
|
|
88
|
+
}
|
|
89
|
+
export function requirePostgresRuntimeFree(manifest) { if (manifest.runtime.length) {
|
|
90
|
+
const f = manifest.runtime[0];
|
|
91
|
+
throw new Error(`FELTDB CUTOVER BLOCKED: ${f.code} at ${f.file}:${f.line} (${manifest.runtime.length} runtime finding${manifest.runtime.length === 1 ? '' : 's'})`);
|
|
92
|
+
} }
|
|
93
|
+
export function configurationMigrationPlan(manifest) {
|
|
94
|
+
const removals = manifest.findings.filter(f => f.kind === 'environment' && f.runtime).map(f => ({ change: 'REMOVE_RUNTIME_CONFIG', file: f.file, line: f.line, reason: 'FeltDB-native runtime', findingId: f.id, requiresApproval: true }));
|
|
95
|
+
const additions = manifest.feltDbRuntime.requiredEnvironment.map(key => ({ change: 'ADD_FELTDB_CONFIG', file: '.env.example', key, reason: `FeltDB ${manifest.feltDbRuntime.mode} runtime`, requiresApproval: true }));
|
|
96
|
+
return [...removals, ...additions];
|
|
97
|
+
}
|
|
98
|
+
export async function verifyPostgresIsolation(manifest, harness) { requirePostgresRuntimeFree(manifest); let startup = false, recovery = false, network = false; if (harness)
|
|
99
|
+
try {
|
|
100
|
+
await harness.startWithPostgresBlocked();
|
|
101
|
+
startup = true;
|
|
102
|
+
await harness.executeWorkflow();
|
|
103
|
+
await harness.restart();
|
|
104
|
+
await harness.verifyRecovery();
|
|
105
|
+
recovery = true;
|
|
106
|
+
network = (await harness.postgresConnectionAttempts()) === 0;
|
|
107
|
+
if (!network)
|
|
108
|
+
throw new Error('PostgreSQL connection attempted during network isolation');
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
await harness.cleanup?.();
|
|
112
|
+
} const count = (code) => manifest.runtime.filter(f => f.code === code).length; return { postgresRuntimeDependency: false, postgresConnections: count('POSTGRES_RUNTIME_CONNECTION') + count('POSTGRES_RUNTIME_CLIENT'), postgresSqlExecutions: count('POSTGRES_RUNTIME_SQL'), postgresEnvironmentVariables: count('POSTGRES_RUNTIME_ENV') + count('POSTGRES_RUNTIME_URL'), postgresStartupDependencies: count('POSTGRES_RUNTIME_STARTUP_DEPENDENCY'), postgresHealthChecks: count('POSTGRES_RUNTIME_HEALTHCHECK'), postgresRuntimeContainers: count('POSTGRES_RUNTIME_CONTAINER'), postgresRuntimeDeploymentReferences: count('POSTGRES_RUNTIME_DEPLOYMENT_DEPENDENCY'), feltDbRuntime: manifest.feltDbRuntime.requiredEnvironment.length > 0 || manifest.feltDbRuntime.mode === 'browser', verifiedWithoutPostgres: startup && recovery && network, startupVerified: startup, recoveryVerified: recovery, networkIsolationVerified: network, runtimeDependencyHash: manifest.runtimeDependencyHash }; }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Analysis, StateContract, StateDomain } from '../types/index.js';
|
|
2
|
+
export declare function discoverStateContract(a: Analysis, configured?: Array<Partial<StateDomain> & {
|
|
3
|
+
name: string;
|
|
4
|
+
authoritativeState: string;
|
|
5
|
+
}>): Promise<StateContract>;
|
|
6
|
+
export declare function verifyObservableState(contract: StateContract, domain: string, input: {
|
|
7
|
+
status?: string;
|
|
8
|
+
currentAction?: string;
|
|
9
|
+
history?: unknown[];
|
|
10
|
+
}): {
|
|
11
|
+
active: boolean;
|
|
12
|
+
empty: boolean;
|
|
13
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { hash } from '../util.js';
|
|
4
|
+
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'target', '.feltdb']);
|
|
5
|
+
async function walk(root, dir = root, out = []) { for (const n of await readdir(dir)) {
|
|
6
|
+
if (SKIP.has(n))
|
|
7
|
+
continue;
|
|
8
|
+
const p = join(dir, n), s = await stat(p);
|
|
9
|
+
if (s.isDirectory())
|
|
10
|
+
await walk(root, p, out);
|
|
11
|
+
else if (/\.[cm]?[jt]sx?$/.test(n))
|
|
12
|
+
out.push(p);
|
|
13
|
+
} return out.sort(); }
|
|
14
|
+
function semantic(line) { if (/current.?action/i.test(line))
|
|
15
|
+
return 'currentAction'; if (/status/i.test(line))
|
|
16
|
+
return 'status'; if (/plans?/i.test(line))
|
|
17
|
+
return 'plan'; if (/results?/i.test(line))
|
|
18
|
+
return 'result'; if (/activit|events?|history/i.test(line))
|
|
19
|
+
return 'activity'; return 'state'; }
|
|
20
|
+
export async function discoverStateContract(a, configured = []) {
|
|
21
|
+
const consumers = [];
|
|
22
|
+
for (const file of await walk(a.sourceRoot)) {
|
|
23
|
+
const text = await readFile(file, 'utf8');
|
|
24
|
+
for (const [i, line] of text.split(/\r?\n/).entries()) {
|
|
25
|
+
if (!/(fetch\(|axios|useQuery|useSWR|poll|subscribe|\.sql\b|SELECT\s|postgres|pg\.)/i.test(line))
|
|
26
|
+
continue;
|
|
27
|
+
const canonical = /feltdb|canonical|stateContract/i.test(line);
|
|
28
|
+
const endpoint = line.match(/['"`]([^'"`]*\/api\/[^'"`]*)['"`]/)?.[1];
|
|
29
|
+
consumers.push({ id: hash([relative(a.sourceRoot, file), i + 1, line]).slice(0, 16), file: relative(a.sourceRoot, file), line: i + 1, surface: file.split('/').pop(), semantic: semantic(line), source: canonical ? 'canonical' : /sql|select|postgres|pg\.|\/api\//i.test(line) ? 'legacy' : 'unknown', endpoint });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const domains = configured.map(d => ({ name: d.name, authoritativeState: d.authoritativeState, history: d.history, fields: d.fields ?? {}, uiSurfaces: d.uiSurfaces ?? consumers.map(c => c.surface), lifecycleStates: d.lifecycleStates ?? ['NEW', 'ACTIVE', 'PAUSED', 'COMPLETED', 'FAILED'], emptyWhen: d.emptyWhen ?? 'authoritative state is absent and history is empty', refresh: d.refresh ?? 'subscription', tenantKey: d.tenantKey }));
|
|
33
|
+
const unresolved = [];
|
|
34
|
+
for (const d of domains) {
|
|
35
|
+
for (const sem of ['status', 'currentAction', 'plan', 'activity']) {
|
|
36
|
+
const matching = consumers.filter(c => c.semantic === sem);
|
|
37
|
+
if (new Set(matching.map(c => c.source)).size > 1)
|
|
38
|
+
unresolved.push(`STATE_CONTRACT_MISMATCH:${d.name}:${sem}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!domains.length)
|
|
42
|
+
unresolved.push('No canonical state domains configured');
|
|
43
|
+
consumers.filter(c => c.source !== 'canonical').forEach(c => unresolved.push(`LEGACY_CONSUMER:${c.file}:${c.line}`));
|
|
44
|
+
const core = { contractVersion: '1.0', generatedAt: new Date().toISOString(), domains, consumers, legacyConsumers: consumers.filter(c => c.source === 'legacy').length, canonicalConsumers: consumers.filter(c => c.source === 'canonical').length, unresolved };
|
|
45
|
+
return { ...core, contractHash: hash({ ...core, generatedAt: undefined }) };
|
|
46
|
+
}
|
|
47
|
+
export function verifyObservableState(contract, domain, input) { const d = contract.domains.find(x => x.name === domain); if (!d)
|
|
48
|
+
throw new Error(`Unknown state domain: ${domain}`); const active = !!input.status && !['NEW', 'EMPTY', 'COMPLETED', 'FAILED'].includes(input.status); const empty = !input.status && !(input.history?.length); if (active && empty)
|
|
49
|
+
throw new Error(`STATE_CONTRACT_MISMATCH:${domain}: active state rendered empty`); return { active, empty }; }
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
export declare const CLASSIFICATIONS: readonly ["STATE", "HISTORY", "RELATIONSHIP", "MATERIALIZED", "BLOB_REFERENCE", "SECRET_REFERENCE", "EXTERNAL_STATE", "OPERATIONAL", "REMOVE"];
|
|
2
|
+
export type Classification = typeof CLASSIFICATIONS[number];
|
|
3
|
+
export type Readiness = 'NEW' | 'ANALYZED' | 'CLASSIFIED' | 'APPROVED' | 'CAPABILITY_READY' | 'PLAN_READY' | 'GENERATED' | 'MIGRATED' | 'STATE_CONTRACT_READY' | 'UI_WIRING_READY' | 'POSTGRES_RUNTIME_FREE' | 'PROVEN' | 'BENCHMARKED' | 'CUTOVER_READY' | 'CUTOVER_COMPLETE';
|
|
4
|
+
export interface Column {
|
|
5
|
+
name: string;
|
|
6
|
+
type: string;
|
|
7
|
+
nullable?: boolean;
|
|
8
|
+
default?: string | null;
|
|
9
|
+
primaryKey?: boolean;
|
|
10
|
+
unique?: boolean;
|
|
11
|
+
sizeBytes?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ForeignKey {
|
|
14
|
+
columns: string[];
|
|
15
|
+
referencedTable: string;
|
|
16
|
+
referencedColumns: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface Entity {
|
|
19
|
+
name: string;
|
|
20
|
+
columns: Column[];
|
|
21
|
+
primaryKey?: string[];
|
|
22
|
+
foreignKeys?: ForeignKey[];
|
|
23
|
+
indexes?: Array<{
|
|
24
|
+
name: string;
|
|
25
|
+
columns: string[];
|
|
26
|
+
unique?: boolean;
|
|
27
|
+
}>;
|
|
28
|
+
kind?: 'table' | 'view';
|
|
29
|
+
}
|
|
30
|
+
export interface SourceFinding {
|
|
31
|
+
file: string;
|
|
32
|
+
line: number;
|
|
33
|
+
kind: string;
|
|
34
|
+
value: string;
|
|
35
|
+
entities: string[];
|
|
36
|
+
}
|
|
37
|
+
export interface Analysis {
|
|
38
|
+
version: 1;
|
|
39
|
+
generatedAt: string;
|
|
40
|
+
sourceRoot: string;
|
|
41
|
+
entities: Entity[];
|
|
42
|
+
findings: SourceFinding[];
|
|
43
|
+
dependencies: Array<{
|
|
44
|
+
from: string;
|
|
45
|
+
to: string;
|
|
46
|
+
}>;
|
|
47
|
+
schemaHash: string;
|
|
48
|
+
}
|
|
49
|
+
export interface ClassifiedEntity {
|
|
50
|
+
entity: string;
|
|
51
|
+
classification: Classification;
|
|
52
|
+
confidence: number;
|
|
53
|
+
reasons: string[];
|
|
54
|
+
requiresApproval: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface ClassificationResult {
|
|
57
|
+
version: 1;
|
|
58
|
+
threshold: number;
|
|
59
|
+
entities: ClassifiedEntity[];
|
|
60
|
+
classificationHash: string;
|
|
61
|
+
approved: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface DomainEntity extends ClassifiedEntity {
|
|
64
|
+
authoritative: boolean;
|
|
65
|
+
target: string;
|
|
66
|
+
secretColumns: string[];
|
|
67
|
+
blobColumns: string[];
|
|
68
|
+
}
|
|
69
|
+
export interface DomainModel {
|
|
70
|
+
version: 1;
|
|
71
|
+
entities: DomainEntity[];
|
|
72
|
+
hash: string;
|
|
73
|
+
}
|
|
74
|
+
export interface PlanStep {
|
|
75
|
+
id: string;
|
|
76
|
+
entity: string;
|
|
77
|
+
classification: Classification;
|
|
78
|
+
dependsOn: string[];
|
|
79
|
+
order: number;
|
|
80
|
+
tenantKey?: string;
|
|
81
|
+
}
|
|
82
|
+
export interface MigrationPlan {
|
|
83
|
+
version: 1;
|
|
84
|
+
planHash: string;
|
|
85
|
+
steps: PlanStep[];
|
|
86
|
+
}
|
|
87
|
+
export interface StateConsumer {
|
|
88
|
+
id: string;
|
|
89
|
+
file: string;
|
|
90
|
+
line: number;
|
|
91
|
+
surface: string;
|
|
92
|
+
semantic: string;
|
|
93
|
+
source: 'legacy' | 'canonical' | 'unknown';
|
|
94
|
+
endpoint?: string;
|
|
95
|
+
}
|
|
96
|
+
export interface StateDomain {
|
|
97
|
+
name: string;
|
|
98
|
+
authoritativeState: string;
|
|
99
|
+
history?: string;
|
|
100
|
+
fields: Record<string, {
|
|
101
|
+
source: string;
|
|
102
|
+
canonical: string;
|
|
103
|
+
}>;
|
|
104
|
+
uiSurfaces: string[];
|
|
105
|
+
lifecycleStates: string[];
|
|
106
|
+
emptyWhen: string;
|
|
107
|
+
refresh: 'subscription' | 'poll' | 'request';
|
|
108
|
+
tenantKey?: string;
|
|
109
|
+
}
|
|
110
|
+
export interface StateContract {
|
|
111
|
+
contractVersion: '1.0';
|
|
112
|
+
contractHash: string;
|
|
113
|
+
generatedAt: string;
|
|
114
|
+
domains: StateDomain[];
|
|
115
|
+
consumers: StateConsumer[];
|
|
116
|
+
legacyConsumers: number;
|
|
117
|
+
canonicalConsumers: number;
|
|
118
|
+
unresolved: string[];
|
|
119
|
+
}
|
|
120
|
+
export interface Capabilities {
|
|
121
|
+
[name: string]: boolean;
|
|
122
|
+
}
|
|
123
|
+
export interface Checkpoint {
|
|
124
|
+
stepId: string;
|
|
125
|
+
status: 'pending' | 'running' | 'complete' | 'failed';
|
|
126
|
+
cursor?: string;
|
|
127
|
+
records: number;
|
|
128
|
+
error?: string;
|
|
129
|
+
}
|
|
130
|
+
export interface MigrationTelemetry {
|
|
131
|
+
timestamp: string;
|
|
132
|
+
stepId: string;
|
|
133
|
+
operation: string;
|
|
134
|
+
records: number;
|
|
135
|
+
durationMs: number;
|
|
136
|
+
tenant?: string;
|
|
137
|
+
}
|
|
138
|
+
export interface MigrationAdapter {
|
|
139
|
+
transaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
140
|
+
put(collection: string, id: string, value: unknown, options: {
|
|
141
|
+
idempotencyKey: string;
|
|
142
|
+
tenant?: string;
|
|
143
|
+
}): Promise<void>;
|
|
144
|
+
append(collection: string, id: string, value: unknown, options: {
|
|
145
|
+
idempotencyKey: string;
|
|
146
|
+
tenant?: string;
|
|
147
|
+
}): Promise<void>;
|
|
148
|
+
relate(collection: string, from: string, to: string, options: {
|
|
149
|
+
idempotencyKey: string;
|
|
150
|
+
tenant?: string;
|
|
151
|
+
}): Promise<void>;
|
|
152
|
+
}
|
|
153
|
+
export interface SourceAdapter {
|
|
154
|
+
read(entity: string, cursor?: string, tenant?: string): AsyncIterable<{
|
|
155
|
+
id: string;
|
|
156
|
+
value: Record<string, unknown>;
|
|
157
|
+
cursor?: string;
|
|
158
|
+
}>;
|
|
159
|
+
}
|
|
160
|
+
export interface FeltDBRuntimeContract {
|
|
161
|
+
mode: 'browser' | 'node' | 'self-hosted' | 'managed';
|
|
162
|
+
requiredEnvironment: string[];
|
|
163
|
+
healthCheck?: string;
|
|
164
|
+
persistence?: string;
|
|
165
|
+
}
|
|
166
|
+
export type DependencyClassification = 'RUNTIME' | 'MIGRATION_SOURCE' | 'LEGACY_FALLBACK' | 'TEST_ONLY' | 'BUILD_ONLY' | 'DEVELOPMENT_ONLY' | 'ROLLBACK_ONLY';
|
|
167
|
+
export type RuntimeFindingCode = 'POSTGRES_RUNTIME_CONNECTION' | 'POSTGRES_RUNTIME_ENV' | 'POSTGRES_RUNTIME_URL' | 'POSTGRES_RUNTIME_CLIENT' | 'POSTGRES_RUNTIME_SQL' | 'POSTGRES_RUNTIME_STARTUP_DEPENDENCY' | 'POSTGRES_RUNTIME_HEALTHCHECK' | 'POSTGRES_RUNTIME_CONTAINER' | 'POSTGRES_RUNTIME_DEPLOYMENT_DEPENDENCY';
|
|
168
|
+
export interface RuntimeDependencyFinding {
|
|
169
|
+
id: string;
|
|
170
|
+
code: RuntimeFindingCode;
|
|
171
|
+
file: string;
|
|
172
|
+
line: number;
|
|
173
|
+
classification: DependencyClassification;
|
|
174
|
+
runtime: boolean;
|
|
175
|
+
kind: 'environment' | 'connection' | 'sql' | 'startup' | 'healthcheck' | 'container' | 'deployment' | 'dependency';
|
|
176
|
+
evidence: string;
|
|
177
|
+
action: string;
|
|
178
|
+
}
|
|
179
|
+
export interface PostgresDependencyManifest {
|
|
180
|
+
version: 1;
|
|
181
|
+
generatedAt: string;
|
|
182
|
+
sourceRoot: string;
|
|
183
|
+
findings: RuntimeDependencyFinding[];
|
|
184
|
+
runtime: RuntimeDependencyFinding[];
|
|
185
|
+
migrationSource: RuntimeDependencyFinding[];
|
|
186
|
+
legacyFallback: RuntimeDependencyFinding[];
|
|
187
|
+
testOnly: RuntimeDependencyFinding[];
|
|
188
|
+
developmentOnly: RuntimeDependencyFinding[];
|
|
189
|
+
buildOnly: RuntimeDependencyFinding[];
|
|
190
|
+
rollbackOnly: RuntimeDependencyFinding[];
|
|
191
|
+
runtimeDependencyHash: string;
|
|
192
|
+
feltDbRuntime: FeltDBRuntimeContract;
|
|
193
|
+
}
|
|
194
|
+
export interface RuntimeDependencyProof {
|
|
195
|
+
postgresRuntimeDependency: boolean;
|
|
196
|
+
postgresConnections: number;
|
|
197
|
+
postgresSqlExecutions: number;
|
|
198
|
+
postgresEnvironmentVariables: number;
|
|
199
|
+
postgresStartupDependencies: number;
|
|
200
|
+
postgresHealthChecks: number;
|
|
201
|
+
postgresRuntimeContainers: number;
|
|
202
|
+
postgresRuntimeDeploymentReferences: number;
|
|
203
|
+
feltDbRuntime: boolean;
|
|
204
|
+
verifiedWithoutPostgres: boolean;
|
|
205
|
+
startupVerified: boolean;
|
|
206
|
+
recoveryVerified: boolean;
|
|
207
|
+
networkIsolationVerified: boolean;
|
|
208
|
+
runtimeDependencyHash: string;
|
|
209
|
+
}
|
|
210
|
+
export interface SherpaConfig {
|
|
211
|
+
sourceRoot: string;
|
|
212
|
+
artifactDir?: string;
|
|
213
|
+
schemaFile: string;
|
|
214
|
+
confidenceThreshold?: number;
|
|
215
|
+
classificationOverrides?: Record<string, Classification>;
|
|
216
|
+
tenantKeys?: Record<string, string>;
|
|
217
|
+
capabilities?: Capabilities;
|
|
218
|
+
requiredCapabilities?: string[];
|
|
219
|
+
stateDomains?: Array<Partial<StateDomain> & {
|
|
220
|
+
name: string;
|
|
221
|
+
authoritativeState: string;
|
|
222
|
+
}>;
|
|
223
|
+
strictStateContract?: boolean;
|
|
224
|
+
blobThresholdBytes?: number;
|
|
225
|
+
feltDbRuntime?: FeltDBRuntimeContract;
|
|
226
|
+
runtimeDependencyOverrides?: Record<string, DependencyClassification>;
|
|
227
|
+
runtimeVerificationModule?: string;
|
|
228
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CLASSIFICATIONS = ['STATE', 'HISTORY', 'RELATIONSHIP', 'MATERIALIZED', 'BLOB_REFERENCE', 'SECRET_REFERENCE', 'EXTERNAL_STATE', 'OPERATIONAL', 'REMOVE'];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StateContract } from '../types/index.js';
|
|
2
|
+
export interface CanonicalStateReader {
|
|
3
|
+
state(domain: string, id: string): Promise<Record<string, unknown> | undefined>;
|
|
4
|
+
history(domain: string, id: string): Promise<unknown[]>;
|
|
5
|
+
relationships?(domain: string, id: string): Promise<unknown[]>;
|
|
6
|
+
}
|
|
7
|
+
export declare function createCanonicalAdapter(contract: StateContract, reader: CanonicalStateReader): {
|
|
8
|
+
read(domainName: string, id: string): Promise<{
|
|
9
|
+
status: unknown;
|
|
10
|
+
currentAction: unknown;
|
|
11
|
+
plan: unknown;
|
|
12
|
+
activity: unknown[];
|
|
13
|
+
history: unknown[];
|
|
14
|
+
result: unknown;
|
|
15
|
+
relationships: unknown[];
|
|
16
|
+
empty: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export function createCanonicalAdapter(contract, reader) { return { async read(domainName, id) { const domain = contract.domains.find(d => d.name === domainName); if (!domain)
|
|
2
|
+
throw new Error(`Unknown canonical domain: ${domainName}`); const [state, history, relationships] = await Promise.all([reader.state(domain.authoritativeState, id), domain.history ? reader.history(domain.history, id) : Promise.resolve([]), reader.relationships?.(domainName, id) ?? Promise.resolve([])]); return { status: state?.status, currentAction: state?.currentAction, plan: state?.plan, activity: history, history, result: state?.result, relationships, empty: !state && history.length === 0 }; } }; }
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function stable(value: unknown): string;
|
|
2
|
+
export declare function hash(value: unknown): string;
|
|
3
|
+
export declare function json<T>(file: string): Promise<T>;
|
|
4
|
+
export declare function writeJson(file: string, value: unknown): Promise<void>;
|
|
5
|
+
export declare const artifact: (dir: string, name: string) => string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
export function stable(value) { if (Array.isArray(value))
|
|
5
|
+
return `[${value.map(stable).join(',')}]`; if (value && typeof value === 'object')
|
|
6
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${JSON.stringify(k)}:${stable(v)}`).join(',')}}`; return JSON.stringify(value); }
|
|
7
|
+
export function hash(value) { return createHash('sha256').update(stable(value)).digest('hex'); }
|
|
8
|
+
export async function json(file) { return JSON.parse(await readFile(file, 'utf8')); }
|
|
9
|
+
export async function writeJson(file, value) { await mkdir(dirname(file), { recursive: true }); await writeFile(file, JSON.stringify(value, null, 2) + '\n'); }
|
|
10
|
+
export const artifact = (dir, name) => join(dir, name);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for executing FeltDB capabilities
|
|
3
|
+
*/
|
|
4
|
+
import type { Capability } from '@feltdb/core';
|
|
5
|
+
export declare function useCapability<TArgs extends any[] = any[], TResult = any>(capability: Capability | undefined): {
|
|
6
|
+
execute: (...args: TArgs) => Promise<any>;
|
|
7
|
+
loading: boolean;
|
|
8
|
+
error: Error | null;
|
|
9
|
+
result: TResult | null;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=useCapability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCapability.d.ts","sourceRoot":"","sources":["../src/useCapability.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,wBAAgB,aAAa,CAAC,KAAK,SAAS,GAAG,EAAE,GAAG,GAAG,EAAE,EAAE,OAAO,GAAG,GAAG,EACtE,UAAU,EAAE,UAAU,GAAG,SAAS;uBAOhB,KAAK;;;;EAsBxB"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for executing FeltDB capabilities
|
|
3
|
+
*/
|
|
4
|
+
import { useCallback, useState } from 'react';
|
|
5
|
+
export function useCapability(capability) {
|
|
6
|
+
const [loading, setLoading] = useState(false);
|
|
7
|
+
const [error, setError] = useState(null);
|
|
8
|
+
const [result, setResult] = useState(null);
|
|
9
|
+
const execute = useCallback(async (...args) => {
|
|
10
|
+
if (!capability) {
|
|
11
|
+
throw new Error('Capability not available');
|
|
12
|
+
}
|
|
13
|
+
setLoading(true);
|
|
14
|
+
setError(null);
|
|
15
|
+
try {
|
|
16
|
+
const output = await capability.execute(...args);
|
|
17
|
+
setResult(output);
|
|
18
|
+
return output;
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
const error = err;
|
|
22
|
+
setError(error);
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
setLoading(false);
|
|
27
|
+
}
|
|
28
|
+
}, [capability]);
|
|
29
|
+
return { execute, loading, error, result };
|
|
30
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for interacting with FeltDB collections
|
|
3
|
+
*/
|
|
4
|
+
import type { Collection } from '@feltdb/core';
|
|
5
|
+
export interface UseCollectionOptions {
|
|
6
|
+
query?: any;
|
|
7
|
+
subscribe?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function useCollection<T = any>(collection: Collection<T>, options?: UseCollectionOptions): {
|
|
10
|
+
data: T[];
|
|
11
|
+
loading: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
insert: (item: T) => Promise<void>;
|
|
14
|
+
update: (id: string, item: Partial<T>) => Promise<void>;
|
|
15
|
+
remove: (id: string) => Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=useCollection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCollection.d.ts","sourceRoot":"","sources":["../src/useCollection.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,aAAa,CAAC,CAAC,GAAG,GAAG,EACnC,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,EACzB,OAAO,CAAC,EAAE,oBAAoB;;;;mBA0Bf,CAAC;iBAaH,MAAM,QAAQ,OAAO,CAAC,CAAC,CAAC;iBAaxB,MAAM;EAapB"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for interacting with FeltDB collections
|
|
3
|
+
*/
|
|
4
|
+
import { useCallback, useState, useEffect } from 'react';
|
|
5
|
+
export function useCollection(collection, options) {
|
|
6
|
+
const [data, setData] = useState([]);
|
|
7
|
+
const [loading, setLoading] = useState(false);
|
|
8
|
+
const [error, setError] = useState(null);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
let active = true;
|
|
11
|
+
setLoading(true);
|
|
12
|
+
collection
|
|
13
|
+
.find(options?.query || {})
|
|
14
|
+
.then((results) => {
|
|
15
|
+
if (active) {
|
|
16
|
+
setData(results);
|
|
17
|
+
setLoading(false);
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
.catch((err) => {
|
|
21
|
+
if (active) {
|
|
22
|
+
setError(err);
|
|
23
|
+
setLoading(false);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
const unsubscribe = options?.subscribe === false ? undefined : collection.subscribe(() => {
|
|
27
|
+
void collection.find(options?.query || {}).then(results => {
|
|
28
|
+
if (active) {
|
|
29
|
+
setData(results);
|
|
30
|
+
setError(null);
|
|
31
|
+
setLoading(false);
|
|
32
|
+
}
|
|
33
|
+
}).catch(err => { if (active)
|
|
34
|
+
setError(err); });
|
|
35
|
+
});
|
|
36
|
+
return () => { active = false; unsubscribe?.(); };
|
|
37
|
+
}, [collection, options?.query, options?.subscribe]);
|
|
38
|
+
const insert = useCallback(async (item) => {
|
|
39
|
+
try {
|
|
40
|
+
await collection.insert(item);
|
|
41
|
+
const updated = await collection.find(options?.query || {});
|
|
42
|
+
setData(updated);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
setError(err);
|
|
46
|
+
}
|
|
47
|
+
}, [collection, options?.query]);
|
|
48
|
+
const update = useCallback(async (id, item) => {
|
|
49
|
+
try {
|
|
50
|
+
await collection.update(id, item);
|
|
51
|
+
const updated = await collection.find(options?.query || {});
|
|
52
|
+
setData(updated);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
setError(err);
|
|
56
|
+
}
|
|
57
|
+
}, [collection, options?.query]);
|
|
58
|
+
const remove = useCallback(async (id) => {
|
|
59
|
+
try {
|
|
60
|
+
await collection.delete(id);
|
|
61
|
+
const updated = await collection.find(options?.query || {});
|
|
62
|
+
setData(updated);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
setError(err);
|
|
66
|
+
}
|
|
67
|
+
}, [collection, options?.query]);
|
|
68
|
+
return { data, loading, error, insert, update, remove };
|
|
69
|
+
}
|