apexrest 1.3.0-beta.1 → 1.3.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 +90 -140
- package/dist/codex-compat/.claude-plugin/marketplace.json +20 -0
- package/dist/codex-compat/plugins/apexrest-apex/.claude-plugin/plugin.json +28 -0
- package/dist/codex-compat/plugins/apexrest-apex/.codex-plugin/plugin.json +7 -7
- package/dist/codex-compat/plugins/apexrest-apex/resources/blocks/generator.json +1 -1
- package/dist/codex-compat/plugins/apexrest-apex/resources/blocks/manifest.json +6 -6
- package/dist/codex-compat/plugins/apexrest-apex/resources/blocks/packages/analytics/status-summary/1.0.0/block.yaml +1 -1
- package/dist/codex-compat/plugins/apexrest-apex/resources/blocks/packages/crud/report-dialog/1.0.0/block.yaml +1 -1
- package/dist/codex-compat/plugins/apexrest-apex/resources/blocks/packages/read/detail/1.0.0/block.yaml +1 -1
- package/dist/codex-compat/plugins/apexrest-apex/resources/blocks/packages/read/filtered-list/1.0.0/block.yaml +1 -1
- package/dist/codex-compat/plugins/apexrest-apex/resources/blocks/packages/read/history/1.0.0/block.yaml +1 -1
- package/dist/codex-compat/plugins/apexrest-apex/resources/blocks/packages/read/master-detail/1.0.0/block.yaml +1 -1
- package/dist/codex-compat/plugins/apexrest-apex/resources/schemas/composer-blueprint.schema.json +5 -4
- package/dist/codex-compat/plugins/apexrest-apex/resources/schemas/composer-contract.schema.json +2 -2
- package/dist/codex-compat/plugins/apexrest-apex/resources/schemas/composer-entity.schema.json +2 -1
- package/dist/codex-compat/plugins/apexrest-apex/resources/schemas/composer-field.schema.json +2 -1
- package/dist/codex-compat/plugins/apexrest-apex/resources/schemas/composer-instance.schema.json +1 -1
- package/dist/codex-compat/plugins/apexrest-apex/resources/schemas/composer-operation.schema.json +1 -1
- package/dist/codex-compat/plugins/apexrest-apex/resources/schemas/composer-plan.schema.json +5 -4
- package/dist/codex-compat/plugins/apexrest-apex/resources/schemas/deploy-plan.schema.json +18 -24
- package/dist/codex-compat/plugins/apexrest-apex/resources/schemas/operations.schema.json +352 -569
- package/dist/codex-compat/plugins/apexrest-apex/resources/schemas/policy.schema.json +21 -0
- package/dist/codex-compat/plugins/apexrest-apex/resources/schemas/project.schema.json +4 -7
- package/dist/codex-compat/plugins/apexrest-apex/resources/templates/blank-app/README.md +0 -2
- package/dist/codex-compat/plugins/apexrest-apex/resources/templates/customer-crm/README.md +1 -3
- package/dist/codex-compat/plugins/apexrest-apex/resources/templates/existing-app/README.md +0 -2
- package/dist/codex-compat/plugins/apexrest-apex/runtime/apexrest.mjs +70 -44
- package/dist/codex-compat/plugins/apexrest-apex/runtime/chunk-5PCC7GZM.mjs +4090 -0
- package/dist/codex-compat/plugins/apexrest-apex/runtime/chunk-BVPAS3TD.mjs +18 -0
- package/dist/codex-compat/plugins/apexrest-apex/runtime/chunk-CVMECW6D.mjs +65 -0
- package/dist/codex-compat/plugins/apexrest-apex/runtime/{chunk-EBBEN4AV.mjs → chunk-JYN3YHP3.mjs} +4220 -10639
- package/dist/codex-compat/plugins/apexrest-apex/runtime/chunk-KGTE7Q5Y.mjs +23 -0
- package/dist/{runtime/chunk-33GE2NWZ.mjs → codex-compat/plugins/apexrest-apex/runtime/chunk-KKRFM7Q7.mjs} +21 -4
- package/dist/codex-compat/plugins/apexrest-apex/runtime/{chunk-2VBZESVC.mjs → chunk-MNU54A7W.mjs} +7 -1
- package/dist/codex-compat/plugins/apexrest-apex/runtime/chunk-N4C2SKCN.mjs +6912 -0
- package/dist/{runtime/chunk-KQ5IDT7Z.mjs → codex-compat/plugins/apexrest-apex/runtime/chunk-P3CVQAZW.mjs} +822 -102
- package/dist/{runtime/chunk-BAHECVGO.mjs → codex-compat/plugins/apexrest-apex/runtime/chunk-QKYF6N6U.mjs} +192 -114
- package/dist/codex-compat/plugins/apexrest-apex/runtime/{chunk-G3KR57BY.mjs → chunk-RBJJNQ5O.mjs} +7 -7
- package/dist/codex-compat/plugins/apexrest-apex/runtime/{chunk-YPLIIQ4Y.mjs → chunk-TMSMKVRP.mjs} +486 -352
- package/dist/codex-compat/plugins/apexrest-apex/runtime/{chunk-2Z3BZF66.mjs → chunk-WPS3CSQJ.mjs} +124 -11
- package/dist/codex-compat/plugins/apexrest-apex/runtime/mcp.mjs +5 -10
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-apexlang/SKILL.md +22 -25
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-apexlang/agents/openai.yaml +3 -3
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-apexlang/references/component-routes.md +1 -5
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-pattern-catalog/SKILL.md +4 -4
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-pattern-catalog/agents/openai.yaml +1 -1
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-pattern-catalog/references/capture-and-curation.md +2 -2
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-safety/SKILL.md +18 -0
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-safety/agents/openai.yaml +4 -0
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-setup/SKILL.md +20 -10
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-setup/agents/openai.yaml +3 -3
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-work/SKILL.md +11 -8
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-work/agents/openai.yaml +3 -3
- package/dist/resources/blocks/generator.json +1 -1
- package/dist/resources/blocks/manifest.json +6 -6
- package/dist/resources/blocks/packages/analytics/status-summary/1.0.0/block.yaml +1 -1
- package/dist/resources/blocks/packages/crud/report-dialog/1.0.0/block.yaml +1 -1
- package/dist/resources/blocks/packages/read/detail/1.0.0/block.yaml +1 -1
- package/dist/resources/blocks/packages/read/filtered-list/1.0.0/block.yaml +1 -1
- package/dist/resources/blocks/packages/read/history/1.0.0/block.yaml +1 -1
- package/dist/resources/blocks/packages/read/master-detail/1.0.0/block.yaml +1 -1
- package/dist/resources/schemas/composer-blueprint.schema.json +5 -4
- package/dist/resources/schemas/composer-contract.schema.json +2 -2
- package/dist/resources/schemas/composer-entity.schema.json +2 -1
- package/dist/resources/schemas/composer-field.schema.json +2 -1
- package/dist/resources/schemas/composer-instance.schema.json +1 -1
- package/dist/resources/schemas/composer-operation.schema.json +1 -1
- package/dist/resources/schemas/composer-plan.schema.json +5 -4
- package/dist/resources/schemas/deploy-plan.schema.json +18 -24
- package/dist/resources/schemas/operations.schema.json +352 -569
- package/dist/resources/schemas/policy.schema.json +21 -0
- package/dist/resources/schemas/project.schema.json +4 -7
- package/dist/resources/templates/blank-app/README.md +0 -2
- package/dist/resources/templates/customer-crm/README.md +1 -3
- package/dist/resources/templates/existing-app/README.md +0 -2
- package/dist/runtime/apexrest.mjs +70 -44
- package/dist/runtime/chunk-5PCC7GZM.mjs +4090 -0
- package/dist/runtime/chunk-BVPAS3TD.mjs +18 -0
- package/dist/runtime/chunk-CVMECW6D.mjs +65 -0
- package/dist/runtime/{chunk-EBBEN4AV.mjs → chunk-JYN3YHP3.mjs} +4220 -10639
- package/dist/runtime/chunk-KGTE7Q5Y.mjs +23 -0
- package/dist/{codex-compat/plugins/apexrest-apex/runtime/chunk-33GE2NWZ.mjs → runtime/chunk-KKRFM7Q7.mjs} +21 -4
- package/dist/runtime/{chunk-2VBZESVC.mjs → chunk-MNU54A7W.mjs} +7 -1
- package/dist/runtime/chunk-N4C2SKCN.mjs +6912 -0
- package/dist/{codex-compat/plugins/apexrest-apex/runtime/chunk-KQ5IDT7Z.mjs → runtime/chunk-P3CVQAZW.mjs} +822 -102
- package/dist/{codex-compat/plugins/apexrest-apex/runtime/chunk-BAHECVGO.mjs → runtime/chunk-QKYF6N6U.mjs} +192 -114
- package/dist/runtime/{chunk-G3KR57BY.mjs → chunk-RBJJNQ5O.mjs} +7 -7
- package/dist/runtime/{chunk-YPLIIQ4Y.mjs → chunk-TMSMKVRP.mjs} +486 -352
- package/dist/runtime/{chunk-2Z3BZF66.mjs → chunk-WPS3CSQJ.mjs} +124 -11
- package/dist/runtime/mcp.mjs +5 -10
- package/package.json +6 -6
- package/README.uk.md +0 -212
- package/dist/codex-compat/plugins/apexrest-apex/resources/panel/index.html +0 -68
- package/dist/codex-compat/plugins/apexrest-apex/resources/panel/panel.css +0 -627
- package/dist/codex-compat/plugins/apexrest-apex/resources/panel/panel.js +0 -861
- package/dist/codex-compat/plugins/apexrest-apex/resources/templates/blank-app/README.uk.md +0 -5
- package/dist/codex-compat/plugins/apexrest-apex/resources/templates/customer-crm/README.uk.md +0 -7
- package/dist/codex-compat/plugins/apexrest-apex/resources/templates/database-control/install.sql +0 -17
- package/dist/codex-compat/plugins/apexrest-apex/resources/templates/existing-app/README.uk.md +0 -9
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__@playwright__test__LICENSE +0 -202
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__@playwright__test__NOTICE +0 -5
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__@types__node__LICENSE +0 -21
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__@types__yauzl__LICENSE +0 -21
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__@typescript__typescript-darwin-arm64__LICENSE +0 -55
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__@typescript__typescript-darwin-arm64__NOTICE.txt +0 -436
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__esbuild__LICENSE.md +0 -21
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__fflate__LICENSE +0 -21
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__marked__LICENSE +0 -44
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__playwright-core__LICENSE +0 -202
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__playwright-core__NOTICE +0 -5
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__playwright__LICENSE +0 -202
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__playwright__NOTICE +0 -5
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__prettier__LICENSE +0 -7
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__typescript__LICENSE +0 -55
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__typescript__NOTICE.txt +0 -436
- package/dist/codex-compat/plugins/apexrest-apex/resources/third-party/node_modules__undici-types__LICENSE +0 -21
- package/dist/codex-compat/plugins/apexrest-apex/runtime/chunk-2BBNJVKW.mjs +0 -1996
- package/dist/codex-compat/plugins/apexrest-apex/runtime/chunk-5JI3XEAK.mjs +0 -14
- package/dist/codex-compat/plugins/apexrest-apex/runtime/chunk-CYPFCUNE.mjs +0 -150
- package/dist/codex-compat/plugins/apexrest-apex/runtime/chunk-ESDVN7DS.mjs +0 -14
- package/dist/codex-compat/plugins/apexrest-apex/runtime/chunk-FTTPESNK.mjs +0 -203
- package/dist/codex-compat/plugins/apexrest-apex/runtime/chunk-HKJVCGQQ.mjs +0 -1302
- package/dist/codex-compat/plugins/apexrest-apex/runtime/chunk-RWGJITHZ.mjs +0 -992
- package/dist/codex-compat/plugins/apexrest-apex/runtime/chunk-TLYYCRQA.mjs +0 -329
- package/dist/codex-compat/plugins/apexrest-apex/runtime/chunk-WXP75UUP.mjs +0 -18
- package/dist/codex-compat/plugins/apexrest-apex/runtime/chunk-YQHHPRJH.mjs +0 -14
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-compose/SKILL.md +0 -18
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-compose/agents/openai.yaml +0 -4
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-compose/references/workflow.md +0 -15
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-database/SKILL.md +0 -9
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-database/agents/openai.yaml +0 -4
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-debug/SKILL.md +0 -9
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-debug/agents/openai.yaml +0 -4
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-deploy/SKILL.md +0 -21
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-deploy/agents/openai.yaml +0 -4
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-install-dependencies/SKILL.md +0 -38
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-install-dependencies/agents/openai.yaml +0 -4
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-menu/SKILL.md +0 -30
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-menu/agents/openai.yaml +0 -4
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-menu/references/cli-catalog.md +0 -30
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-panel/SKILL.md +0 -14
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-panel/agents/openai.yaml +0 -4
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-project/SKILL.md +0 -11
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-project/agents/openai.yaml +0 -4
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-review/SKILL.md +0 -11
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-review/agents/openai.yaml +0 -4
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-test/SKILL.md +0 -29
- package/dist/codex-compat/plugins/apexrest-apex/skills/apexrest-test/agents/openai.yaml +0 -4
- package/dist/resources/panel/index.html +0 -68
- package/dist/resources/panel/panel.css +0 -627
- package/dist/resources/panel/panel.js +0 -861
- package/dist/resources/templates/blank-app/README.uk.md +0 -5
- package/dist/resources/templates/customer-crm/README.uk.md +0 -7
- package/dist/resources/templates/database-control/install.sql +0 -17
- package/dist/resources/templates/existing-app/README.uk.md +0 -9
- package/dist/resources/third-party/node_modules__@playwright__test__LICENSE +0 -202
- package/dist/resources/third-party/node_modules__@playwright__test__NOTICE +0 -5
- package/dist/resources/third-party/node_modules__@types__node__LICENSE +0 -21
- package/dist/resources/third-party/node_modules__@types__yauzl__LICENSE +0 -21
- package/dist/resources/third-party/node_modules__@typescript__typescript-darwin-arm64__LICENSE +0 -55
- package/dist/resources/third-party/node_modules__@typescript__typescript-darwin-arm64__NOTICE.txt +0 -436
- package/dist/resources/third-party/node_modules__esbuild__LICENSE.md +0 -21
- package/dist/resources/third-party/node_modules__fflate__LICENSE +0 -21
- package/dist/resources/third-party/node_modules__marked__LICENSE +0 -44
- package/dist/resources/third-party/node_modules__playwright-core__LICENSE +0 -202
- package/dist/resources/third-party/node_modules__playwright-core__NOTICE +0 -5
- package/dist/resources/third-party/node_modules__playwright__LICENSE +0 -202
- package/dist/resources/third-party/node_modules__playwright__NOTICE +0 -5
- package/dist/resources/third-party/node_modules__prettier__LICENSE +0 -7
- package/dist/resources/third-party/node_modules__typescript__LICENSE +0 -55
- package/dist/resources/third-party/node_modules__typescript__NOTICE.txt +0 -436
- package/dist/resources/third-party/node_modules__undici-types__LICENSE +0 -21
- package/dist/runtime/chunk-2BBNJVKW.mjs +0 -1996
- package/dist/runtime/chunk-5JI3XEAK.mjs +0 -14
- package/dist/runtime/chunk-CYPFCUNE.mjs +0 -150
- package/dist/runtime/chunk-ESDVN7DS.mjs +0 -14
- package/dist/runtime/chunk-FTTPESNK.mjs +0 -203
- package/dist/runtime/chunk-HKJVCGQQ.mjs +0 -1302
- package/dist/runtime/chunk-RWGJITHZ.mjs +0 -992
- package/dist/runtime/chunk-TLYYCRQA.mjs +0 -329
- package/dist/runtime/chunk-WXP75UUP.mjs +0 -18
- package/dist/runtime/chunk-YQHHPRJH.mjs +0 -14
|
@@ -0,0 +1,4090 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
ArtifactService,
|
|
4
|
+
DeploymentService,
|
|
5
|
+
OWNED_TEXT_LIMIT,
|
|
6
|
+
SyncStore,
|
|
7
|
+
TestService,
|
|
8
|
+
blueprintSchema,
|
|
9
|
+
browserPreferences,
|
|
10
|
+
catalogRead,
|
|
11
|
+
catalogSearch,
|
|
12
|
+
checkpoint,
|
|
13
|
+
deployPlanSchema,
|
|
14
|
+
digest,
|
|
15
|
+
documentText,
|
|
16
|
+
freeze,
|
|
17
|
+
journal,
|
|
18
|
+
loadCatalog,
|
|
19
|
+
materialize,
|
|
20
|
+
planDigest,
|
|
21
|
+
planLimits,
|
|
22
|
+
planSchema,
|
|
23
|
+
readDocument,
|
|
24
|
+
readPlan,
|
|
25
|
+
recoveryPlan,
|
|
26
|
+
resolvePackages,
|
|
27
|
+
safePath,
|
|
28
|
+
semanticDigest,
|
|
29
|
+
stagePlan,
|
|
30
|
+
stateSchema,
|
|
31
|
+
validate
|
|
32
|
+
} from "./chunk-TMSMKVRP.mjs";
|
|
33
|
+
import {
|
|
34
|
+
VERSION
|
|
35
|
+
} from "./chunk-RBJJNQ5O.mjs";
|
|
36
|
+
import {
|
|
37
|
+
OracleAdapter,
|
|
38
|
+
configureConnection,
|
|
39
|
+
configureSqlcl,
|
|
40
|
+
connections,
|
|
41
|
+
databaseTransport,
|
|
42
|
+
editConnection,
|
|
43
|
+
environment,
|
|
44
|
+
identifier,
|
|
45
|
+
installSources,
|
|
46
|
+
isProductionTarget,
|
|
47
|
+
loadProject,
|
|
48
|
+
managedHome,
|
|
49
|
+
oracle_exports,
|
|
50
|
+
ordsUrl,
|
|
51
|
+
ordsUsername,
|
|
52
|
+
parse,
|
|
53
|
+
policy,
|
|
54
|
+
projectInit,
|
|
55
|
+
projectInspect,
|
|
56
|
+
refName,
|
|
57
|
+
relativePath,
|
|
58
|
+
requireTrust,
|
|
59
|
+
resolveConnection,
|
|
60
|
+
resourceRoot,
|
|
61
|
+
runProcess,
|
|
62
|
+
runtimeState,
|
|
63
|
+
savedConnectionName,
|
|
64
|
+
sqlclConfig,
|
|
65
|
+
sqlclMode,
|
|
66
|
+
sqlclRestriction,
|
|
67
|
+
updatePolicy
|
|
68
|
+
} from "./chunk-N4C2SKCN.mjs";
|
|
69
|
+
import {
|
|
70
|
+
external_exports
|
|
71
|
+
} from "./chunk-JYN3YHP3.mjs";
|
|
72
|
+
import {
|
|
73
|
+
Fault,
|
|
74
|
+
artifactPage,
|
|
75
|
+
canonical,
|
|
76
|
+
contained,
|
|
77
|
+
exists,
|
|
78
|
+
failure,
|
|
79
|
+
hash,
|
|
80
|
+
inventory,
|
|
81
|
+
readJson,
|
|
82
|
+
redact,
|
|
83
|
+
sanitized,
|
|
84
|
+
success,
|
|
85
|
+
writeJson
|
|
86
|
+
} from "./chunk-WPS3CSQJ.mjs";
|
|
87
|
+
|
|
88
|
+
// packages/core/src/jobs.ts
|
|
89
|
+
import path from "node:path";
|
|
90
|
+
import { spawn } from "node:child_process";
|
|
91
|
+
import { randomUUID } from "node:crypto";
|
|
92
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
93
|
+
var JOB_WAIT_MAX_SECONDS = 120;
|
|
94
|
+
var jobOperations = [
|
|
95
|
+
"compose.plan",
|
|
96
|
+
"compose.materialize",
|
|
97
|
+
"apex.sync",
|
|
98
|
+
"apex.generate",
|
|
99
|
+
"apex.export",
|
|
100
|
+
"apex.validate",
|
|
101
|
+
"deploy.plan",
|
|
102
|
+
"deploy.apply",
|
|
103
|
+
"ship.apply",
|
|
104
|
+
"test.run"
|
|
105
|
+
];
|
|
106
|
+
var JobService = class {
|
|
107
|
+
constructor(ctx) {
|
|
108
|
+
this.ctx = ctx;
|
|
109
|
+
}
|
|
110
|
+
ctx;
|
|
111
|
+
async enqueue(operation, input) {
|
|
112
|
+
await requireTrust(this.ctx.root);
|
|
113
|
+
if (!jobOperations.includes(operation))
|
|
114
|
+
throw new Fault("INVALID_JOB_OPERATION", "Operation cannot run as a background job.", 2);
|
|
115
|
+
const id = randomUUID(), root = await contained(this.ctx.root, ".apexrest/jobs/" + id);
|
|
116
|
+
await writeJson(path.join(root, "request.json"), {
|
|
117
|
+
id,
|
|
118
|
+
operation,
|
|
119
|
+
input: { ...input, project: this.ctx.root }
|
|
120
|
+
});
|
|
121
|
+
await writeJson(path.join(root, "state.json"), {
|
|
122
|
+
id,
|
|
123
|
+
status: "queued",
|
|
124
|
+
operation,
|
|
125
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
126
|
+
});
|
|
127
|
+
return id;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Run a job inside this process: same request/state files and heartbeat as a
|
|
131
|
+
* detached worker, so apexrest_job observes it identically, but the warm
|
|
132
|
+
* SQLcl session pool and capability caches are reused. Use only for work
|
|
133
|
+
* whose interruption leaves no database write unresolved.
|
|
134
|
+
*/
|
|
135
|
+
async startInline(operation, input, execute) {
|
|
136
|
+
const id = await this.enqueue(operation, input);
|
|
137
|
+
const run = executeJob(this.ctx, id, execute).catch(async (error) => {
|
|
138
|
+
await failQueuedJob(this.ctx.root, id, error).catch(() => void 0);
|
|
139
|
+
});
|
|
140
|
+
inlineJobs.set(id, run);
|
|
141
|
+
void run.finally(() => inlineJobs.delete(id));
|
|
142
|
+
return {
|
|
143
|
+
jobId: id,
|
|
144
|
+
status: "queued",
|
|
145
|
+
runner: "in-process",
|
|
146
|
+
nextAction: "Wait for this job with apexrest_job action:status and the same jobId; never rerun the operation to fetch results."
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async start(operation, input, runtime) {
|
|
150
|
+
const id = await this.enqueue(operation, input);
|
|
151
|
+
try {
|
|
152
|
+
const worker = spawn(process.execPath, [runtime, "--job-worker", this.ctx.root, id], {
|
|
153
|
+
cwd: this.ctx.root,
|
|
154
|
+
env: process.env,
|
|
155
|
+
detached: true,
|
|
156
|
+
stdio: "ignore",
|
|
157
|
+
windowsHide: true
|
|
158
|
+
});
|
|
159
|
+
await new Promise((resolve, reject) => {
|
|
160
|
+
worker.once("spawn", resolve);
|
|
161
|
+
worker.once("error", reject);
|
|
162
|
+
});
|
|
163
|
+
worker.unref();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
await failQueuedJob(this.ctx.root, id, error).catch(() => void 0);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
jobId: id,
|
|
170
|
+
status: "queued",
|
|
171
|
+
runner: "detached-worker",
|
|
172
|
+
nextAction: "Wait for this job with apexrest_job action:status and the same jobId; never rerun the operation to fetch results. Cancellation does not imply database rollback."
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
async status(id, waitSeconds = 0, signal) {
|
|
176
|
+
parse(external_exports.uuid(), id);
|
|
177
|
+
parse(external_exports.number().int().min(0).max(JOB_WAIT_MAX_SECONDS), waitSeconds);
|
|
178
|
+
const root = await contained(this.ctx.root, ".apexrest/jobs/" + id);
|
|
179
|
+
const deadline = Date.now() + waitSeconds * 1e3;
|
|
180
|
+
for (; ; ) {
|
|
181
|
+
const state = await readJson(path.join(root, "state.json"));
|
|
182
|
+
if (!["queued", "running"].includes(state.status)) return state;
|
|
183
|
+
if (Date.parse(state.updatedAt) + 6e4 < Date.now())
|
|
184
|
+
return {
|
|
185
|
+
...state,
|
|
186
|
+
status: "outcome_unknown",
|
|
187
|
+
nextAction: "Worker heartbeat expired. Reconcile target before retrying."
|
|
188
|
+
};
|
|
189
|
+
const remaining = deadline - Date.now();
|
|
190
|
+
if (remaining <= 0 || signal?.aborted) return state;
|
|
191
|
+
await delay(Math.min(250, remaining), void 0, { signal }).catch((error) => {
|
|
192
|
+
if (!signal?.aborted) throw error;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async cancel(id) {
|
|
197
|
+
await requireTrust(this.ctx.root);
|
|
198
|
+
parse(external_exports.uuid(), id);
|
|
199
|
+
const state = await this.status(id);
|
|
200
|
+
if (!["queued", "running"].includes(state.status)) return state;
|
|
201
|
+
await writeJson(await contained(this.ctx.root, ".apexrest/jobs/" + id + "/cancel.json"), {
|
|
202
|
+
requestedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
203
|
+
});
|
|
204
|
+
return { jobId: id, status: "cancellation_requested", rollbackConfirmed: false };
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
var inlineJobs = /* @__PURE__ */ new Map();
|
|
208
|
+
async function settleInlineJobs() {
|
|
209
|
+
await Promise.allSettled([...inlineJobs.values()]);
|
|
210
|
+
}
|
|
211
|
+
async function failQueuedJob(projectRoot, id, error) {
|
|
212
|
+
parse(external_exports.uuid(), id);
|
|
213
|
+
const file = await contained(projectRoot, ".apexrest/jobs/" + id + "/state.json");
|
|
214
|
+
if (!await exists(file)) return false;
|
|
215
|
+
const state = await readJson(file);
|
|
216
|
+
if (state.status !== "queued") return false;
|
|
217
|
+
const operation = typeof state.operation === "string" ? state.operation : "job";
|
|
218
|
+
await writeJson(file, {
|
|
219
|
+
id,
|
|
220
|
+
operation,
|
|
221
|
+
status: "failed",
|
|
222
|
+
result: failure(operation, error),
|
|
223
|
+
nextAction: "The worker did not start this operation. Resolve the diagnostic, then start a new job.",
|
|
224
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
225
|
+
});
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
function jobOutcome(result) {
|
|
229
|
+
const value = result;
|
|
230
|
+
if (!value || typeof value !== "object" || value.ok !== false) return "completed";
|
|
231
|
+
return typeof value.status === "string" && !["queued", "running", "completed", "succeeded"].includes(value.status) ? value.status : "failed";
|
|
232
|
+
}
|
|
233
|
+
async function executeJob(ctx, id, execute) {
|
|
234
|
+
await requireTrust(ctx.root);
|
|
235
|
+
parse(external_exports.uuid(), id);
|
|
236
|
+
const root = await contained(ctx.root, ".apexrest/jobs/" + id);
|
|
237
|
+
const request = await readJson(path.join(root, "request.json"));
|
|
238
|
+
const controller = new AbortController();
|
|
239
|
+
let done = false;
|
|
240
|
+
let phase;
|
|
241
|
+
const pulse = async () => {
|
|
242
|
+
if (done) return;
|
|
243
|
+
if (await exists(path.join(root, "cancel.json"))) controller.abort();
|
|
244
|
+
if (!done)
|
|
245
|
+
await writeJson(path.join(root, "state.json"), {
|
|
246
|
+
id,
|
|
247
|
+
operation: request.operation,
|
|
248
|
+
status: "running",
|
|
249
|
+
...phase ? { phase } : {},
|
|
250
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
await pulse();
|
|
254
|
+
let pending = Promise.resolve();
|
|
255
|
+
const schedule = () => {
|
|
256
|
+
pending = pending.then(pulse).catch(() => {
|
|
257
|
+
controller.abort();
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
const timer = setInterval(schedule, 2e3), timeout = setTimeout(() => controller.abort(), 9e5);
|
|
261
|
+
const progress = (next) => {
|
|
262
|
+
phase = next;
|
|
263
|
+
schedule();
|
|
264
|
+
};
|
|
265
|
+
try {
|
|
266
|
+
let result;
|
|
267
|
+
try {
|
|
268
|
+
result = await execute(request.operation, request.input, controller.signal, progress);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
result = failure(request.operation, error);
|
|
271
|
+
}
|
|
272
|
+
done = true;
|
|
273
|
+
clearInterval(timer);
|
|
274
|
+
clearTimeout(timeout);
|
|
275
|
+
await pending;
|
|
276
|
+
await writeJson(path.join(root, "state.json"), {
|
|
277
|
+
id,
|
|
278
|
+
operation: request.operation,
|
|
279
|
+
status: jobOutcome(result),
|
|
280
|
+
...phase ? { phase } : {},
|
|
281
|
+
result,
|
|
282
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
283
|
+
});
|
|
284
|
+
} finally {
|
|
285
|
+
done = true;
|
|
286
|
+
clearInterval(timer);
|
|
287
|
+
clearTimeout(timeout);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// packages/core/src/composer/service.ts
|
|
292
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
293
|
+
|
|
294
|
+
// packages/core/src/metadata.ts
|
|
295
|
+
var metadataOffset = external_exports.number().int().min(0).max(1e5);
|
|
296
|
+
var metadataLimit = external_exports.number().int().min(1).max(100);
|
|
297
|
+
var metadataRequest = external_exports.strictObject({
|
|
298
|
+
kind: external_exports.enum([
|
|
299
|
+
"objects",
|
|
300
|
+
"columns",
|
|
301
|
+
"constraints",
|
|
302
|
+
"constraint-columns",
|
|
303
|
+
"signatures",
|
|
304
|
+
"applications",
|
|
305
|
+
"pages"
|
|
306
|
+
]),
|
|
307
|
+
schema: identifier,
|
|
308
|
+
name: identifier.optional(),
|
|
309
|
+
offset: metadataOffset.default(0),
|
|
310
|
+
limit: metadataLimit.default(30)
|
|
311
|
+
});
|
|
312
|
+
var metadataRequests = external_exports.array(metadataRequest).min(1).max(8);
|
|
313
|
+
var metadataBatchRequest = external_exports.strictObject({ requests: metadataRequests });
|
|
314
|
+
var metadataInputSchema = external_exports.strictObject({
|
|
315
|
+
kind: metadataRequest.shape.kind.optional(),
|
|
316
|
+
schema: metadataRequest.shape.schema.optional(),
|
|
317
|
+
name: metadataRequest.shape.name,
|
|
318
|
+
offset: metadataOffset.optional(),
|
|
319
|
+
limit: metadataLimit.optional(),
|
|
320
|
+
requests: metadataRequests.optional()
|
|
321
|
+
});
|
|
322
|
+
var queries = {
|
|
323
|
+
objects: "select object_name, object_type from all_objects where owner=:p_owner and object_type in ('TABLE','VIEW','PACKAGE') and (:p_name is null or object_name=:p_name) order by object_name, object_type",
|
|
324
|
+
columns: "select table_name,column_name,data_type,data_length,char_length,char_used,data_precision,data_scale,nullable,column_id from all_tab_columns where owner=:p_owner and table_name=:p_name order by column_id",
|
|
325
|
+
constraints: "select table_name,constraint_name,constraint_type,r_owner,r_constraint_name,status,validated from all_constraints where owner=:p_owner and table_name=:p_name order by constraint_name",
|
|
326
|
+
"constraint-columns": "select c.table_name,c.constraint_name,c.constraint_type,c.status,c.validated,cc.column_name,cc.position,c.r_owner,c.r_constraint_name,rc.table_name referenced_table,rcc.column_name referenced_column from all_constraints c join all_cons_columns cc on cc.owner=c.owner and cc.constraint_name=c.constraint_name and cc.table_name=c.table_name left join all_constraints rc on rc.owner=c.r_owner and rc.constraint_name=c.r_constraint_name and rc.owner=:p_owner left join all_cons_columns rcc on rcc.owner=rc.owner and rcc.constraint_name=rc.constraint_name and rcc.position=cc.position where c.owner=:p_owner and c.table_name=:p_name order by c.constraint_name,cc.position",
|
|
327
|
+
signatures: "select package_name,object_name,argument_name,position,sequence,data_level,in_out,data_type,type_owner,type_name,type_subname,defaulted,overload,subprogram_id from all_arguments where owner=:p_owner and package_name=:p_name order by object_name,overload,sequence",
|
|
328
|
+
applications: "select application_id,application_name,alias from apex_applications where owner=:p_owner and application_id=:p_app_id order by application_id",
|
|
329
|
+
pages: "select application_id,page_id,page_name,page_alias from apex_application_pages where application_id=:p_app_id and workspace=:p_workspace order by page_id"
|
|
330
|
+
};
|
|
331
|
+
async function metadataRead(adapter, env2, connection, value) {
|
|
332
|
+
const input = parse(external_exports.union([metadataRequest, metadataBatchRequest]), value);
|
|
333
|
+
const batch = "requests" in input;
|
|
334
|
+
const requests = batch ? input.requests : [input];
|
|
335
|
+
for (const r of requests) {
|
|
336
|
+
if (r.schema !== env2.parsingSchema)
|
|
337
|
+
throw new Fault("SCHEMA_DENIED", "Metadata is restricted to the configured parsing schema.", 4);
|
|
338
|
+
if (["columns", "constraints", "constraint-columns", "signatures"].includes(r.kind) && !r.name)
|
|
339
|
+
throw new Fault("OBJECT_REQUIRED", "Select a specific object first.", 2);
|
|
340
|
+
}
|
|
341
|
+
await adapter.verifyTarget(env2, connection);
|
|
342
|
+
const query = (r) => ({
|
|
343
|
+
sql: queries[r.kind] + " offset :p_offset rows fetch next :p_limit rows only",
|
|
344
|
+
bindings: {
|
|
345
|
+
p_owner: r.schema,
|
|
346
|
+
p_name: r.name ?? "",
|
|
347
|
+
p_app_id: env2.applicationId,
|
|
348
|
+
p_workspace: env2.workspace,
|
|
349
|
+
p_offset: r.offset,
|
|
350
|
+
p_limit: r.limit
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
const page2 = (r, rows2) => ({
|
|
354
|
+
dataClassification: "untrusted_database_content",
|
|
355
|
+
rows: rows2,
|
|
356
|
+
offset: r.offset,
|
|
357
|
+
nextOffset: rows2.length === r.limit ? r.offset + r.limit : null
|
|
358
|
+
});
|
|
359
|
+
if (!batch) {
|
|
360
|
+
const q = query(input);
|
|
361
|
+
return page2(input, await adapter.jsonQuery(q.sql, connection, q.bindings));
|
|
362
|
+
}
|
|
363
|
+
const rows = await adapter.jsonQueryBatch(requests.map(query), connection);
|
|
364
|
+
const results = requests.map((r, index) => ({
|
|
365
|
+
index,
|
|
366
|
+
kind: r.kind,
|
|
367
|
+
...r.name ? { name: r.name } : {},
|
|
368
|
+
...page2(r, rows[index] ?? [])
|
|
369
|
+
}));
|
|
370
|
+
if (rows.length !== requests.length)
|
|
371
|
+
throw new Fault("EMPTY_QUERY_RESULT", "SQLcl did not answer every metadata request.", 1);
|
|
372
|
+
return { results, targetVerifiedOnce: true };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// packages/core/src/composer/planner.ts
|
|
376
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
377
|
+
|
|
378
|
+
// packages/core/src/composer/reader.ts
|
|
379
|
+
function declarations(source2) {
|
|
380
|
+
if (Buffer.byteLength(source2) > 8 * 1024 * 1024)
|
|
381
|
+
throw new Fault("TRANSFORM_UNSUPPORTED", "Source exceeds the bounded reader limit.", 5);
|
|
382
|
+
const lines = [...source2.matchAll(/[^\r\n]*(?:\r\n|\r|\n|$)/g)].filter((m) => m[0].length);
|
|
383
|
+
const stack = [], out = [];
|
|
384
|
+
let fence = false;
|
|
385
|
+
for (const line of lines) {
|
|
386
|
+
const text = line[0].replace(/[\r\n]+$/, "");
|
|
387
|
+
if (/^\s*```/.test(text)) {
|
|
388
|
+
fence = !fence;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (fence) continue;
|
|
392
|
+
const open = text.match(/^( *)([A-Za-z][\w]*)(?:\s+(.*?))?\s*\(\s*$/);
|
|
393
|
+
if (open) stack.push({ kind: open[2], key: open[3] ?? "", start: line.index, depth: open[1].length });
|
|
394
|
+
else if (/^ *\)\s*$/.test(text)) {
|
|
395
|
+
const node2 = stack.pop();
|
|
396
|
+
if (!node2 || node2.depth !== text.indexOf(")"))
|
|
397
|
+
throw new Fault("TRANSFORM_UNSUPPORTED", "Unbalanced declaration boundary.", 5);
|
|
398
|
+
const end = line.index + line[0].length;
|
|
399
|
+
out.push({ ...node2, end, digest: hash(source2.slice(node2.start, end)) });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (fence || stack.length) throw new Fault("TRANSFORM_UNSUPPORTED", "Unclosed literal or declaration.", 5);
|
|
403
|
+
return out.sort((a, b) => a.start - b.start);
|
|
404
|
+
}
|
|
405
|
+
function inventorySymbols(sources) {
|
|
406
|
+
const pages = /* @__PURE__ */ new Set(), symbols = /* @__PURE__ */ new Set();
|
|
407
|
+
for (const source2 of Object.values(sources))
|
|
408
|
+
for (const node2 of declarations(source2)) {
|
|
409
|
+
if (node2.kind === "page" && /^\d+$/.test(node2.key)) pages.add(Number(node2.key));
|
|
410
|
+
if (node2.key) symbols.add(node2.key.toUpperCase());
|
|
411
|
+
}
|
|
412
|
+
return { pages, symbols };
|
|
413
|
+
}
|
|
414
|
+
function editSpans(source2, edits) {
|
|
415
|
+
let result = source2, previous = source2.length + 1;
|
|
416
|
+
for (const edit of [...edits].sort((a, b) => b.start - a.start)) {
|
|
417
|
+
if (edit.start < 0 || edit.end > previous || edit.start > edit.end || hash(source2.slice(edit.start, edit.end)) !== edit.expectedDigest)
|
|
418
|
+
throw new Fault("TRANSFORM_CONFLICT", "Overlapping or stale structural edit.", 5);
|
|
419
|
+
result = result.slice(0, edit.start) + edit.content + result.slice(edit.end);
|
|
420
|
+
previous = edit.start;
|
|
421
|
+
}
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
function threeWay(base2, local, next) {
|
|
425
|
+
if (local === base2) return next;
|
|
426
|
+
if (next === base2 || local === next) return local;
|
|
427
|
+
const variants = [base2, local, next], roots = variants.map(declarations);
|
|
428
|
+
const rootDepth = roots[0]?.[0]?.depth;
|
|
429
|
+
if (rootDepth !== void 0 && roots.every((nodes) => nodes[0]?.depth === rootDepth)) {
|
|
430
|
+
const children = roots.map((nodes) => nodes.filter((node2) => node2.depth === rootDepth + 4));
|
|
431
|
+
const maps = children.map(
|
|
432
|
+
(nodes, i) => new Map(
|
|
433
|
+
nodes.map((node2) => [
|
|
434
|
+
node2.kind + ":" + node2.key,
|
|
435
|
+
{ node: node2, content: variants[i].slice(node2.start, node2.end) }
|
|
436
|
+
])
|
|
437
|
+
)
|
|
438
|
+
);
|
|
439
|
+
if (maps.some((map, i) => map.size !== children[i].length))
|
|
440
|
+
throw new Fault("COMPOSITION_CONFLICT", "Repeated sibling anchors are unsupported.", 5);
|
|
441
|
+
const scaffold = (value, nodes) => editSpans(
|
|
442
|
+
value,
|
|
443
|
+
nodes.map((node2) => ({ ...node2, expectedDigest: node2.digest, content: "" }))
|
|
444
|
+
);
|
|
445
|
+
const wrappers = variants.map((v, i) => scaffold(v, children[i]));
|
|
446
|
+
const wrapper = mergeLines(wrappers[0], wrappers[1], wrappers[2]);
|
|
447
|
+
const edits = [];
|
|
448
|
+
const additions = [];
|
|
449
|
+
for (const key of /* @__PURE__ */ new Set([...maps[1].keys(), ...maps[2].keys()])) {
|
|
450
|
+
const b = maps[0].get(key), l = maps[1].get(key), n = maps[2].get(key);
|
|
451
|
+
let content;
|
|
452
|
+
if (!b) {
|
|
453
|
+
if (l && n && l.content !== n.content)
|
|
454
|
+
throw new Fault("COMPOSITION_CONFLICT", "New declaration collides with local source.", 5);
|
|
455
|
+
content = l?.content ?? n?.content;
|
|
456
|
+
} else if (!n) {
|
|
457
|
+
if (l && l.content !== b.content)
|
|
458
|
+
throw new Fault("COMPOSITION_CONFLICT", "Removing an edited declaration requires detach.", 5);
|
|
459
|
+
content = "";
|
|
460
|
+
} else if (!l) {
|
|
461
|
+
if (n.content !== b.content)
|
|
462
|
+
throw new Fault(
|
|
463
|
+
"COMPOSITION_CONFLICT",
|
|
464
|
+
"A locally removed declaration changed in the generator.",
|
|
465
|
+
5
|
|
466
|
+
);
|
|
467
|
+
} else content = threeWay(b.content, l.content, n.content);
|
|
468
|
+
if (l && content !== void 0 && content !== l.content)
|
|
469
|
+
edits.push({ ...l.node, expectedDigest: l.node.digest, content });
|
|
470
|
+
else if (!l && !b && content) additions.push(content);
|
|
471
|
+
}
|
|
472
|
+
const translate = (offset) => {
|
|
473
|
+
let skipped = 0;
|
|
474
|
+
for (const child of children[1]) {
|
|
475
|
+
if (child.start - skipped > offset) break;
|
|
476
|
+
skipped += child.end - child.start;
|
|
477
|
+
}
|
|
478
|
+
return offset + skipped;
|
|
479
|
+
};
|
|
480
|
+
let cursor = 0;
|
|
481
|
+
const originalLines = wrappers[1].split("\n"), mergedLines = wrapper.split("\n");
|
|
482
|
+
originalLines.forEach((line, i) => {
|
|
483
|
+
if (line !== mergedLines[i]) {
|
|
484
|
+
const start = translate(cursor), end = start + line.length;
|
|
485
|
+
edits.push({ start, end, expectedDigest: hash(local.slice(start, end)), content: mergedLines[i] });
|
|
486
|
+
}
|
|
487
|
+
cursor += line.length + 1;
|
|
488
|
+
});
|
|
489
|
+
if (additions.length) {
|
|
490
|
+
const root = roots[1][0], close = local.lastIndexOf(")", root.end - 1), start = local.lastIndexOf("\n", close) + 1;
|
|
491
|
+
edits.push({ start, end: start, expectedDigest: hash(""), content: additions.join("") });
|
|
492
|
+
}
|
|
493
|
+
return editSpans(local, edits);
|
|
494
|
+
}
|
|
495
|
+
return mergeLines(base2, local, next);
|
|
496
|
+
}
|
|
497
|
+
function mergeLines(base2, local, next) {
|
|
498
|
+
if (local === base2) return next;
|
|
499
|
+
if (next === base2 || local === next) return local;
|
|
500
|
+
const literals = [base2, local, next].map(
|
|
501
|
+
(source2) => [...source2.matchAll(/```[^\r\n]*[\r\n]+[\s\S]*?^[ \t]*```/gm)].map((match2) => match2[0])
|
|
502
|
+
);
|
|
503
|
+
if (literals[0].length !== literals[1].length || literals[0].length !== literals[2].length || literals[0].some(
|
|
504
|
+
(value, i) => value !== literals[1][i] && value !== literals[2][i] && literals[1][i] !== literals[2][i]
|
|
505
|
+
))
|
|
506
|
+
throw new Fault(
|
|
507
|
+
"COMPOSITION_CONFLICT",
|
|
508
|
+
"Concurrent changes to an opaque code literal require explicit review.",
|
|
509
|
+
5
|
|
510
|
+
);
|
|
511
|
+
const b = base2.split("\n"), l = local.split("\n"), n = next.split("\n");
|
|
512
|
+
if (b.length !== l.length || b.length !== n.length)
|
|
513
|
+
throw new Fault(
|
|
514
|
+
"COMPOSITION_CONFLICT",
|
|
515
|
+
"Structural changes require explicit adoption or conflict resolution.",
|
|
516
|
+
5
|
|
517
|
+
);
|
|
518
|
+
return b.map((line, i) => {
|
|
519
|
+
if (l[i] === line) return n[i];
|
|
520
|
+
if (n[i] === line || l[i] === n[i]) return l[i];
|
|
521
|
+
throw new Fault("COMPOSITION_CONFLICT", `Both local and generated source changed line ${i + 1}.`, 5);
|
|
522
|
+
}).join("\n");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// packages/core/src/composer/binding.ts
|
|
526
|
+
function keyFields(entity) {
|
|
527
|
+
return entity.read.key.map((key) => {
|
|
528
|
+
const entries = Object.entries(entity.read.fields).filter(
|
|
529
|
+
([name, field]) => name === key || field.column === key
|
|
530
|
+
);
|
|
531
|
+
if (entries.length > 1)
|
|
532
|
+
throw new Fault("KEY_MAPPING_AMBIGUOUS", "A key part must identify exactly one projected field.", 5);
|
|
533
|
+
const entry2 = entries[0];
|
|
534
|
+
if (!entry2) throw new Fault("KEY_MAPPING_MISSING", "Every key part must map to a projected field.", 5);
|
|
535
|
+
return entry2[0];
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
var unsafeSql = () => new Fault("CONTRACT_SQL_UNSAFE", "Row predicates accept reviewed expressions only.", 5);
|
|
539
|
+
var untrustedContext = () => new Fault("AUTH_CONTEXT_UNTRUSTED", "Row access may only use server-owned APEX session bindings.", 5);
|
|
540
|
+
var serverBinds = /* @__PURE__ */ new Set(["APP_USER", "APP_ID", "APP_SESSION"]);
|
|
541
|
+
var safeFunctions = /* @__PURE__ */ new Set([
|
|
542
|
+
"UPPER",
|
|
543
|
+
"LOWER",
|
|
544
|
+
"TRIM",
|
|
545
|
+
"TRUNC",
|
|
546
|
+
"NVL",
|
|
547
|
+
"COALESCE",
|
|
548
|
+
"LENGTH",
|
|
549
|
+
"SUBSTR",
|
|
550
|
+
"INSTR",
|
|
551
|
+
"TO_CHAR",
|
|
552
|
+
"TO_NUMBER",
|
|
553
|
+
"TO_DATE",
|
|
554
|
+
"APEX_AUTHORIZATION.IS_AUTHORIZED"
|
|
555
|
+
]);
|
|
556
|
+
var groupingWords = /* @__PURE__ */ new Set(["AND", "OR", "NOT", "IN"]);
|
|
557
|
+
var deniedWords = /^(?:SELECT|WITH|COMMIT|ROLLBACK|SAVEPOINT|GRANT|REVOKE|INSERT|UPDATE|DELETE|MERGE|DROP|ALTER|CREATE|TRUNCATE|EXECUTE|IMMEDIATE|HOST|CONNECT|BEGIN|DECLARE|CALL|LOCK)$/;
|
|
558
|
+
var deniedOwners = /^(?:DBMS_|UTL_|WWV_|OWA_|APEX_(?!AUTHORIZATION$)|(?:OWA|HTP|HTF|SYS)$)/;
|
|
559
|
+
var tokens = {
|
|
560
|
+
space: /[ \t]+/y,
|
|
561
|
+
string: /'(?:[^']|'')*'/y,
|
|
562
|
+
number: /\d+(?:\.\d+)?/y,
|
|
563
|
+
bind: /:([A-Za-z][A-Za-z0-9_]*)/y,
|
|
564
|
+
name: /[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*){0,2}/y,
|
|
565
|
+
operator: /<>|!=|\^=|<=|>=|=|<|>|\(|\)|,|\+|-|\*|\//y
|
|
566
|
+
};
|
|
567
|
+
function match(pattern, value, at) {
|
|
568
|
+
pattern.lastIndex = at;
|
|
569
|
+
return pattern.exec(value);
|
|
570
|
+
}
|
|
571
|
+
function expression(value) {
|
|
572
|
+
if (!value.trim() || value.length > 4e3) throw unsafeSql();
|
|
573
|
+
if (/:\s*"/.test(value)) throw untrustedContext();
|
|
574
|
+
if (/[\x00-\x08\x0a-\x1f\x7f;`&#$"@{}\[\]|]|--|\/\*|\*\//.test(value)) throw unsafeSql();
|
|
575
|
+
let at = 0, depth = 0;
|
|
576
|
+
while (at < value.length) {
|
|
577
|
+
let found;
|
|
578
|
+
if (found = match(tokens.space, value, at)) at += found[0].length;
|
|
579
|
+
else if (found = match(tokens.string, value, at)) at += found[0].length;
|
|
580
|
+
else if (found = match(tokens.bind, value, at)) {
|
|
581
|
+
if (!serverBinds.has(found[1].toUpperCase())) throw untrustedContext();
|
|
582
|
+
at += found[0].length;
|
|
583
|
+
} else if (found = match(tokens.name, value, at)) {
|
|
584
|
+
const name = found[0].toUpperCase(), parts = name.split(".");
|
|
585
|
+
at += found[0].length;
|
|
586
|
+
if (parts.some((part) => deniedWords.test(part)) || deniedOwners.test(parts[0])) {
|
|
587
|
+
if (!safeFunctions.has(name)) throw unsafeSql();
|
|
588
|
+
}
|
|
589
|
+
if (value[at] === "'") throw unsafeSql();
|
|
590
|
+
const next = value.slice(at).match(/^[ \t]*(.)/)?.[1];
|
|
591
|
+
if (next === "(" && !safeFunctions.has(name) && !groupingWords.has(name)) throw unsafeSql();
|
|
592
|
+
if (next !== "(" && safeFunctions.has(name) && name.includes(".")) throw unsafeSql();
|
|
593
|
+
} else if (found = match(tokens.number, value, at)) at += found[0].length;
|
|
594
|
+
else if (found = match(tokens.operator, value, at)) {
|
|
595
|
+
if (found[0] === "(") depth++;
|
|
596
|
+
if (found[0] === ")" && --depth < 0) throw unsafeSql();
|
|
597
|
+
at += found[0].length;
|
|
598
|
+
} else if (value[at] === ":") throw untrustedContext();
|
|
599
|
+
else throw unsafeSql();
|
|
600
|
+
}
|
|
601
|
+
if (depth !== 0) throw unsafeSql();
|
|
602
|
+
return value;
|
|
603
|
+
}
|
|
604
|
+
function oracleName(value) {
|
|
605
|
+
const text = String(value ?? "");
|
|
606
|
+
return /^".*"$/.test(text) ? text.slice(1, -1) : text.toUpperCase();
|
|
607
|
+
}
|
|
608
|
+
var sameName = (a, b) => oracleName(a) === oracleName(b);
|
|
609
|
+
function oracleType(value) {
|
|
610
|
+
return String(value ?? "").toUpperCase().replace(/\(\s*\d+(?:\s*,\s*\d+)?\s*\)/g, "").replace(/\s+/g, " ").trim();
|
|
611
|
+
}
|
|
612
|
+
var entry = (record, name) => Object.entries(record).find(([key]) => sameName(key, name))?.[1];
|
|
613
|
+
function bind(blueprint, instance, metadata) {
|
|
614
|
+
const ref = instance.bindings.records;
|
|
615
|
+
if (!ref.startsWith("entity:"))
|
|
616
|
+
throw new Fault("BINDING_MISSING", "Expected an explicit entity binding.", 5);
|
|
617
|
+
const entity = blueprint.entities[ref.slice(7)];
|
|
618
|
+
if (!entity) throw new Fault("BINDING_MISSING", "Entity binding is absent.", 5);
|
|
619
|
+
if (!Object.keys(entity.read.fields).length || Object.keys(entity.read.fields).length > 32)
|
|
620
|
+
throw new Fault("CONTRACT_FIELD_LIMIT", "Entities support 1\u201332 scalar fields.", 5);
|
|
621
|
+
const fieldNames = Object.keys(entity.read.fields);
|
|
622
|
+
if (fieldNames.some((field) => !/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(field)) || new Set(fieldNames.map((field) => field.toUpperCase())).size !== fieldNames.length)
|
|
623
|
+
throw new Fault(
|
|
624
|
+
"FIELD_SYMBOL_COLLISION",
|
|
625
|
+
"Field names must form unique case-insensitive APEX item suffixes.",
|
|
626
|
+
5
|
|
627
|
+
);
|
|
628
|
+
const keys = keyFields(entity), auth = blueprint.contracts[entity.authorization.readContract];
|
|
629
|
+
if (new Set(keys).size !== keys.length)
|
|
630
|
+
throw new Fault("KEY_MAPPING_DUPLICATE", "Each ordered key part must occur once.", 5);
|
|
631
|
+
if (!auth || auth.kind !== "authorization" || !auth.rowPredicate)
|
|
632
|
+
throw new Fault("AUTH_CONTRACT_MISSING", "A reviewed server row-read contract is required.", 5);
|
|
633
|
+
const predicate = expression(auth.rowPredicate);
|
|
634
|
+
if (entity.read.kind === "oracle-view") {
|
|
635
|
+
const key = blueprint.contracts[entity.read.keyContract ?? ""];
|
|
636
|
+
if (!key || key.kind !== "key" || JSON.stringify(key.fields) !== JSON.stringify(keys))
|
|
637
|
+
throw new Fault("KEY_CONTRACT_MISSING", "Views require an explicit ordered key contract.", 5);
|
|
638
|
+
}
|
|
639
|
+
if (new Set(Object.values(entity.read.fields).map((f) => f.column.toUpperCase())).size !== Object.keys(entity.read.fields).length)
|
|
640
|
+
throw new Fault("COLUMN_COLLISION", "Projected field mappings must be unique.", 5);
|
|
641
|
+
for (const name of [
|
|
642
|
+
...instance.parameters.editableFields,
|
|
643
|
+
instance.parameters.groupField,
|
|
644
|
+
instance.parameters.filterField,
|
|
645
|
+
instance.parameters.detailField,
|
|
646
|
+
instance.parameters.timeField,
|
|
647
|
+
instance.parameters.parentField
|
|
648
|
+
].filter(Boolean))
|
|
649
|
+
if (!entity.read.fields[name])
|
|
650
|
+
throw new Fault("FIELD_MAPPING_MISSING", "A selected field is not in the entity contract.", 5);
|
|
651
|
+
const writable = instance.parameters.createEnabled || instance.parameters.editEnabled;
|
|
652
|
+
let command;
|
|
653
|
+
let writeExpression;
|
|
654
|
+
if (writable) {
|
|
655
|
+
if (keys.length !== 1)
|
|
656
|
+
throw new Fault("COMPOSITE_WRITE_UNSUPPORTED", "Write CRUD requires one scalar key.", 5);
|
|
657
|
+
const version = entity.capabilities.optimisticLock?.field;
|
|
658
|
+
if (!version || entity.read.fields[version]?.type !== "integer" || !entity.read.fields[version] || entity.read.fields[version].nullable || entity.read.fields[keys[0]].nullable)
|
|
659
|
+
throw new Fault("OPTIMISTIC_LOCK_MISSING", "Persisted key/version fields must be non-null.", 5);
|
|
660
|
+
if (instance.parameters.editableFields.some((field) => keys.includes(field) || field === version))
|
|
661
|
+
throw new Fault("MASS_ASSIGNMENT_DENIED", "Key and row version are server managed.", 5);
|
|
662
|
+
if (!["string", "integer", "decimal"].includes(entity.read.fields[keys[0]].type) || instance.parameters.editableFields.some((f) => entity.read.fields[f].type === "boolean"))
|
|
663
|
+
throw new Fault(
|
|
664
|
+
"WRITE_TYPE_UNSUPPORTED",
|
|
665
|
+
"This write adapter supports text/numeric keys and scalar text/number/date/timestamp fields.",
|
|
666
|
+
5
|
|
667
|
+
);
|
|
668
|
+
const ref2 = instance.bindings.saveRecord;
|
|
669
|
+
command = ref2?.startsWith("command:") ? blueprint.commands[ref2.slice(8)] : void 0;
|
|
670
|
+
if (!command)
|
|
671
|
+
throw new Fault("COMMAND_BINDING_MISSING", "Write mode requires an explicit API command.", 5);
|
|
672
|
+
const signature = blueprint.contracts[command.signatureRef], writeAuth = blueprint.contracts[command.authorizationContract], errors = blueprint.contracts[command.errorContract];
|
|
673
|
+
if (!signature || signature.kind !== "command" || signature.transaction !== "caller-owned" || !signature.parameters || !writeAuth || writeAuth.kind !== "authorization" || !errors || errors.kind !== "errors" || command.authorizationContract !== entity.authorization.writeContract)
|
|
674
|
+
throw new Fault(
|
|
675
|
+
"COMMAND_CONTRACT_MISSING",
|
|
676
|
+
"Write mode requires exact reviewed signature, authorization and error contracts.",
|
|
677
|
+
5
|
|
678
|
+
);
|
|
679
|
+
if (!writeAuth.expression)
|
|
680
|
+
throw new Fault(
|
|
681
|
+
"AUTH_CONTRACT_MISSING",
|
|
682
|
+
"Write authorization requires a reviewed server expression.",
|
|
683
|
+
5
|
|
684
|
+
);
|
|
685
|
+
writeExpression = expression(writeAuth.expression);
|
|
686
|
+
if (command.outputs.recordKey.from === command.outputs.recordVersion.from)
|
|
687
|
+
throw new Fault(
|
|
688
|
+
"COMMAND_ARGUMENT_MISMATCH",
|
|
689
|
+
"Record key and version require distinct API output arguments.",
|
|
690
|
+
5
|
|
691
|
+
);
|
|
692
|
+
const supplied = /* @__PURE__ */ new Set([
|
|
693
|
+
...Object.keys(command.inputs),
|
|
694
|
+
...Object.values(command.outputs).map((o) => o.from)
|
|
695
|
+
]);
|
|
696
|
+
if (Object.keys(signature.parameters).some((p) => !supplied.has(p)) || [...supplied].some((p) => !signature.parameters[p]))
|
|
697
|
+
throw new Fault("COMMAND_ARGUMENT_MISMATCH", "Every exact signature argument must be mapped.", 5);
|
|
698
|
+
const types = (field) => {
|
|
699
|
+
const type = entity.read.fields[field]?.type;
|
|
700
|
+
return type === "integer" || type === "decimal" ? ["NUMBER", "PLS_INTEGER", "BINARY_INTEGER"] : type === "date" ? ["DATE"] : type === "timestamp" ? ["TIMESTAMP"] : ["VARCHAR2", "CHAR", "NVARCHAR2", "NCHAR"];
|
|
701
|
+
};
|
|
702
|
+
for (const [argument, mapping] of Object.entries(command.inputs)) {
|
|
703
|
+
const field = mapping.from.replace(/^record\./, "");
|
|
704
|
+
if (mapping.mode === "in-out" && field !== keys[0])
|
|
705
|
+
throw new Fault(
|
|
706
|
+
"COMMAND_ARGUMENT_MISMATCH",
|
|
707
|
+
"Only the record key supports IN OUT in this adapter.",
|
|
708
|
+
5
|
|
709
|
+
);
|
|
710
|
+
if (!types(field).includes(signature.parameters[argument]?.type ?? ""))
|
|
711
|
+
throw new Fault(
|
|
712
|
+
"COMMAND_ARGUMENT_MISMATCH",
|
|
713
|
+
"API argument scalar datatype differs from the field contract.",
|
|
714
|
+
5
|
|
715
|
+
);
|
|
716
|
+
if (!mapping.from.startsWith("record.") || !entity.read.fields[field] || signature.parameters[argument]?.mode !== mapping.mode)
|
|
717
|
+
throw new Fault(
|
|
718
|
+
"COMMAND_ARGUMENT_MISMATCH",
|
|
719
|
+
"API argument mode or field mapping does not match its contract.",
|
|
720
|
+
5
|
|
721
|
+
);
|
|
722
|
+
if (!keys.includes(field) && field !== version && !instance.parameters.editableFields.includes(field))
|
|
723
|
+
throw new Fault(
|
|
724
|
+
"COMMAND_INPUT_UNAVAILABLE",
|
|
725
|
+
"API inputs must map to an editable field or the managed record key/version.",
|
|
726
|
+
5
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
if (!types(keys[0]).includes(signature.parameters[command.outputs.recordKey.from]?.type ?? "") || !types(version).includes(signature.parameters[command.outputs.recordVersion.from]?.type ?? ""))
|
|
730
|
+
throw new Fault("COMMAND_ARGUMENT_MISMATCH", "Key/version output datatypes differ from the entity.", 5);
|
|
731
|
+
if (!Object.values(command.inputs).some((m) => m.from === "record." + version && m.mode === "in"))
|
|
732
|
+
throw new Fault("OPTIMISTIC_LOCK_MISSING", "Expected version must be passed explicitly to the API.", 5);
|
|
733
|
+
for (const output of Object.values(command.outputs))
|
|
734
|
+
if (!["out", "in-out"].includes(signature.parameters[output.from]?.mode ?? ""))
|
|
735
|
+
throw new Fault("COMMAND_ARGUMENT_MISMATCH", "API key/version outputs must be OUT or IN OUT.", 5);
|
|
736
|
+
if (metadata) {
|
|
737
|
+
const rows = entry(metadata.signatures, command.package)?.filter(
|
|
738
|
+
(row) => sameName(row.OBJECT_NAME, command.procedure)
|
|
739
|
+
) ?? [];
|
|
740
|
+
const overloads = new Set(rows.map((row) => String(row.OVERLOAD ?? row.SUBPROGRAM_ID ?? "")));
|
|
741
|
+
if (overloads.size !== 1 && !signature.overload)
|
|
742
|
+
throw new Fault("COMMAND_OVERLOAD_AMBIGUOUS", "An exact reviewed API overload is required.", 5);
|
|
743
|
+
const selected = rows.filter(
|
|
744
|
+
(row) => !signature.overload || String(row.OVERLOAD ?? "") === signature.overload
|
|
745
|
+
);
|
|
746
|
+
if (new Set(selected.map((row) => String(row.SUBPROGRAM_ID))).size !== 1 || selected.length !== Object.keys(signature.parameters).length || selected.some((row) => Number(row.DATA_LEVEL ?? 0) !== 0 || Number(row.POSITION) === 0))
|
|
747
|
+
throw new Fault(
|
|
748
|
+
"COMMAND_ARGUMENT_MISMATCH",
|
|
749
|
+
"The complete live scalar procedure signature must match the reviewed contract.",
|
|
750
|
+
5
|
|
751
|
+
);
|
|
752
|
+
for (const [argument, spec] of Object.entries(signature.parameters)) {
|
|
753
|
+
const row = selected.find(
|
|
754
|
+
(r) => sameName(r.ARGUMENT_NAME, argument) && (!signature.overload || String(r.OVERLOAD ?? "") === signature.overload)
|
|
755
|
+
);
|
|
756
|
+
if (!row || String(row.IN_OUT).toLowerCase().replace(/\s*\/\s*|\s+/g, "-") !== spec.mode || oracleType(row.DATA_TYPE) !== spec.type || row.DEFAULTED === "Y" !== spec.defaulted)
|
|
757
|
+
throw new Fault(
|
|
758
|
+
"COMMAND_ARGUMENT_MISMATCH",
|
|
759
|
+
"Live API signature differs from its reviewed contract.",
|
|
760
|
+
5
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (metadata) {
|
|
766
|
+
const object = entry(metadata.objects, entity.read.object);
|
|
767
|
+
if (!object) throw new Fault("OBJECT_BINDING_MISSING", "The selected Oracle object was not verified.", 5);
|
|
768
|
+
for (const field of Object.values(entity.read.fields)) {
|
|
769
|
+
const column = object.columns.find((row) => sameName(row.COLUMN_NAME, field.column));
|
|
770
|
+
if (!column || column.NULLABLE === "Y" && !field.nullable)
|
|
771
|
+
throw new Fault("COLUMN_CONTRACT_MISMATCH", "Live field nullability or column mapping differs.", 5);
|
|
772
|
+
const expected = ["integer", "decimal"].includes(field.type) ? ["NUMBER", "FLOAT"] : field.type === "date" ? ["DATE"] : field.type === "timestamp" ? ["TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "TIMESTAMP WITH LOCAL TIME ZONE"] : ["VARCHAR2", "CHAR", "NVARCHAR2", "NCHAR"];
|
|
773
|
+
if (!expected.includes(oracleType(column.DATA_TYPE)))
|
|
774
|
+
throw new Fault("COLUMN_TYPE_UNSUPPORTED", "Live datatype needs an explicit supported adapter.", 5);
|
|
775
|
+
if (field.maxLength && Number(column.CHAR_LENGTH ?? column.DATA_LENGTH) > field.maxLength)
|
|
776
|
+
throw new Fault("COLUMN_CONTRACT_MISMATCH", "Producer length exceeds consumer capacity.", 5);
|
|
777
|
+
if (field.precision !== void 0 && (column.DATA_PRECISION == null || Number(column.DATA_PRECISION) > field.precision) || field.scale !== void 0 && Number(column.DATA_SCALE) !== field.scale)
|
|
778
|
+
throw new Fault(
|
|
779
|
+
"COLUMN_CONTRACT_MISMATCH",
|
|
780
|
+
"Live numeric precision or scale differs from the field contract.",
|
|
781
|
+
5
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
if (entity.read.kind === "oracle-table") {
|
|
785
|
+
const primary = object.constraints.find(
|
|
786
|
+
(row) => row.CONSTRAINT_TYPE === "P" && row.STATUS === "ENABLED" && row.VALIDATED === "VALIDATED"
|
|
787
|
+
);
|
|
788
|
+
const columns = object.constraintColumns.filter((row) => row.CONSTRAINT_NAME === primary?.CONSTRAINT_NAME).sort((a, b) => Number(a.POSITION) - Number(b.POSITION)).map((row) => oracleName(row.COLUMN_NAME));
|
|
789
|
+
if (!primary || JSON.stringify(columns) !== JSON.stringify(keys.map((k) => oracleName(entity.read.fields[k].column))))
|
|
790
|
+
throw new Fault(
|
|
791
|
+
"PRIMARY_KEY_MISMATCH",
|
|
792
|
+
"Live enabled/validated primary key differs from the contract.",
|
|
793
|
+
5
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return { entity, keys, predicate, command, writable, entityRef: ref, writeExpression };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// packages/core/src/composer/emitter.ts
|
|
801
|
+
var indent = (value, depth = 4) => value.split("\n").map((line) => line ? " ".repeat(depth) + line : "").join("\n");
|
|
802
|
+
var group = (key, value) => `${key} {
|
|
803
|
+
${indent(value)}
|
|
804
|
+
}`;
|
|
805
|
+
var node = (kind, key, value) => `${kind} ${key} (
|
|
806
|
+
${indent(value)}
|
|
807
|
+
)
|
|
808
|
+
`;
|
|
809
|
+
var scalar = (value) => {
|
|
810
|
+
if (/[\r\n\x00-\x1f\x7f{}()`]/.test(value))
|
|
811
|
+
throw new Fault("PARAMETER_UNSUPPORTED", "Labels must be single-line literal values.", 2);
|
|
812
|
+
if (/[<>]|&[A-Za-z0-9_$#]+\.|#[A-Za-z0-9_$]+#|^\s*@/.test(value))
|
|
813
|
+
throw new Fault(
|
|
814
|
+
"LABEL_UNSAFE",
|
|
815
|
+
"Titles and labels cannot contain HTML, &ITEM. or #NAME# substitutions, or a leading @ reference.",
|
|
816
|
+
2
|
|
817
|
+
);
|
|
818
|
+
return value;
|
|
819
|
+
};
|
|
820
|
+
var sqlLiteral = (value) => {
|
|
821
|
+
if (/[\x00-\x1f\x7f`]/.test(value))
|
|
822
|
+
throw new Fault("LITERAL_UNSAFE", "Literal values must be single-line text without backticks.", 2);
|
|
823
|
+
return "'" + value.replaceAll("'", "''") + "'";
|
|
824
|
+
};
|
|
825
|
+
var code = (language, source2) => `
|
|
826
|
+
\`\`\`${language}
|
|
827
|
+
${indent(source2, 4)}
|
|
828
|
+
\`\`\``;
|
|
829
|
+
var layout = (sequence, slot = "body") => group("layout", `sequence: ${sequence}
|
|
830
|
+
slot: ${slot}`);
|
|
831
|
+
var appearance = (template) => group("appearance", `template: @/${template}
|
|
832
|
+
templateOptions: #DEFAULT#`);
|
|
833
|
+
var page = (allocation, title, body, dialog = false) => node(
|
|
834
|
+
"page",
|
|
835
|
+
String(dialog ? allocation.dialog : allocation.page),
|
|
836
|
+
[
|
|
837
|
+
`name: ${scalar(title)}`,
|
|
838
|
+
`alias: ${allocation.prefix.toUpperCase()}${dialog ? "_EDIT" : ""}`,
|
|
839
|
+
`title: ${scalar(title)}`,
|
|
840
|
+
group(
|
|
841
|
+
"appearance",
|
|
842
|
+
dialog ? "pageMode: modalDialog\ndialogTemplate: @/modal-dialog\ntemplateOptions: #DEFAULT#" : "pageTemplate: @/standard\ntemplateOptions: #DEFAULT#"
|
|
843
|
+
),
|
|
844
|
+
group("security", "pageAccessProtection: argumentsMustHaveChecksum"),
|
|
845
|
+
body
|
|
846
|
+
].join("\n")
|
|
847
|
+
);
|
|
848
|
+
var source = (sql, submit) => group(
|
|
849
|
+
"source",
|
|
850
|
+
`location: localDatabase
|
|
851
|
+
type: sqlQuery
|
|
852
|
+
${submit ? `pageItemsToSubmit: ${submit}
|
|
853
|
+
` : ""}sqlQuery:${code("sql", sql)}`
|
|
854
|
+
);
|
|
855
|
+
var button = (key, label, region, behavior) => node(
|
|
856
|
+
"button",
|
|
857
|
+
key,
|
|
858
|
+
[
|
|
859
|
+
`buttonName: ${key.replaceAll("-", "_").toUpperCase()}`,
|
|
860
|
+
`label: ${scalar(label)}`,
|
|
861
|
+
group("layout", `sequence: 10
|
|
862
|
+
region: @${region}
|
|
863
|
+
slot: NEXT`),
|
|
864
|
+
group("appearance", "buttonTemplate: @/text\ntemplateOptions: #DEFAULT#"),
|
|
865
|
+
group("behavior", behavior)
|
|
866
|
+
].join("\n")
|
|
867
|
+
);
|
|
868
|
+
function dynamic(key, event, selection, actions) {
|
|
869
|
+
return node(
|
|
870
|
+
"dynamicAction",
|
|
871
|
+
key,
|
|
872
|
+
`name: ${key}
|
|
873
|
+
${group("execution", "sequence: 10")}
|
|
874
|
+
${group("when", `event: ${event}
|
|
875
|
+
${selection}`)}
|
|
876
|
+
${actions}`
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
function refresh(key, region, sequence) {
|
|
880
|
+
return node(
|
|
881
|
+
"action",
|
|
882
|
+
key,
|
|
883
|
+
`action: refresh
|
|
884
|
+
${group("affectedElements", `selectionType: region
|
|
885
|
+
region: @${region}`)}
|
|
886
|
+
${group("execution", `sequence: ${sequence}
|
|
887
|
+
fireOnInit: false`)}`
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
function jsAction(key, javascript) {
|
|
891
|
+
return node(
|
|
892
|
+
"action",
|
|
893
|
+
key,
|
|
894
|
+
`action: executeJsCode
|
|
895
|
+
${group("settings", `jsCode:${code("javascript", javascript)}`)}
|
|
896
|
+
${group("execution", "sequence: 10\nfireOnInit: false")}`
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
function process2(key, point, sql) {
|
|
900
|
+
return node(
|
|
901
|
+
"process",
|
|
902
|
+
key,
|
|
903
|
+
`name: ${key}
|
|
904
|
+
type: executeCode
|
|
905
|
+
${group("source", `plsqlCode:${code("plsql", sql)}`)}
|
|
906
|
+
${group("execution", `sequence: 10
|
|
907
|
+
point: ${point}`)}`
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
function item(name, field, sequence, hidden, required) {
|
|
911
|
+
return node(
|
|
912
|
+
"pageItem",
|
|
913
|
+
name,
|
|
914
|
+
[
|
|
915
|
+
`type: ${hidden ? "hidden" : "textField"}`,
|
|
916
|
+
hidden ? "" : group("label", `label: ${scalar(field)}
|
|
917
|
+
alignment: left`),
|
|
918
|
+
group("layout", `sequence: ${sequence}
|
|
919
|
+
region: @form
|
|
920
|
+
slot: regionBody`),
|
|
921
|
+
hidden ? group("security", "sessionStateProtection: checksumRequiredSessionLevel") : group("appearance", "template: @/optional-floating\ntemplateOptions: #DEFAULT#\nwidth: 32"),
|
|
922
|
+
hidden ? "" : group("validation", `valueRequired: ${required}`)
|
|
923
|
+
].filter(Boolean).join("\n")
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
function summaryRegion(blueprint, instance, allocation) {
|
|
927
|
+
const { entity, predicate } = bind(blueprint, instance), field = entity.read.fields[instance.parameters.groupField ?? ""];
|
|
928
|
+
if (!field) throw new Fault("GROUP_FIELD_REQUIRED", "Summary blocks require a mapped grouping field.", 5);
|
|
929
|
+
return node(
|
|
930
|
+
"region",
|
|
931
|
+
allocation.prefix + "-summary",
|
|
932
|
+
[
|
|
933
|
+
`name: ${scalar(instance.parameters.title)}`,
|
|
934
|
+
"type: cards",
|
|
935
|
+
source(
|
|
936
|
+
`select ${field.column} ID, ${field.column} TITLE, to_char(count(*)) STATUS from ${entity.read.object} where (${predicate}) group by ${field.column}`
|
|
937
|
+
),
|
|
938
|
+
layout(30),
|
|
939
|
+
appearance("cards-container"),
|
|
940
|
+
group("advanced", `htmlDomId: ${allocation.prefix}_summary`),
|
|
941
|
+
group("card", "primaryKeyColumn1: ID"),
|
|
942
|
+
group("title", "column: TITLE"),
|
|
943
|
+
group("body", "column: STATUS")
|
|
944
|
+
].join("\n")
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
function render(blueprint, id, instance, block, allocation, summaries = []) {
|
|
948
|
+
const binding = bind(blueprint, instance), { entity, keys, predicate, command, writable } = binding;
|
|
949
|
+
const fields = Object.entries(entity.read.fields).sort(([a], [b]) => a < b ? -1 : 1);
|
|
950
|
+
const file = (number) => `pages/p${String(number).padStart(5, "0")}-${allocation.prefix}${number === allocation.dialog ? "_edit" : ""}.apx`;
|
|
951
|
+
if (block.renderer === "status-summary")
|
|
952
|
+
return {
|
|
953
|
+
[file(allocation.page)]: page(
|
|
954
|
+
allocation,
|
|
955
|
+
instance.parameters.title,
|
|
956
|
+
summaryRegion(blueprint, instance, allocation)
|
|
957
|
+
)
|
|
958
|
+
};
|
|
959
|
+
if (block.renderer === "read-only-detail") {
|
|
960
|
+
if (keys.length !== 1)
|
|
961
|
+
throw new Fault(
|
|
962
|
+
"COMPOSITE_DETAIL_UNSUPPORTED",
|
|
963
|
+
"This detail adapter requires one scalar route key.",
|
|
964
|
+
5
|
|
965
|
+
);
|
|
966
|
+
const keyItem = `P${allocation.page}_${keys[0].toUpperCase()}`;
|
|
967
|
+
let body2 = node(
|
|
968
|
+
"region",
|
|
969
|
+
"form",
|
|
970
|
+
`name: ${scalar(instance.parameters.title)}
|
|
971
|
+
type: staticContent
|
|
972
|
+
${layout(10)}
|
|
973
|
+
${appearance("standard")}`
|
|
974
|
+
);
|
|
975
|
+
body2 += item(keyItem, keys[0], 0, true, false);
|
|
976
|
+
for (const [field] of fields.filter(([f]) => !keys.includes(f)))
|
|
977
|
+
body2 += node(
|
|
978
|
+
"pageItem",
|
|
979
|
+
`P${allocation.page}_${field.toUpperCase()}`,
|
|
980
|
+
`type: displayOnly
|
|
981
|
+
${group("label", `label: ${field}`)}
|
|
982
|
+
${group("layout", `sequence: ${(fields.findIndex(([f]) => f === field) + 1) * 10}
|
|
983
|
+
region: @form
|
|
984
|
+
slot: regionBody`)}
|
|
985
|
+
${appearance("optional")}`
|
|
986
|
+
);
|
|
987
|
+
body2 += process2(
|
|
988
|
+
allocation.prefix + "-detail",
|
|
989
|
+
"beforeHeader",
|
|
990
|
+
`begin
|
|
991
|
+
if :${keyItem} is not null then
|
|
992
|
+
select ${fields.map(([, f]) => f.column).join(", ")} into ${fields.map(([f]) => ":P" + allocation.page + "_" + f.toUpperCase()).join(", ")} from ${entity.read.object} where (${predicate}) and ${entity.read.fields[keys[0]].column}=:${keyItem};
|
|
993
|
+
end if;
|
|
994
|
+
end;`
|
|
995
|
+
);
|
|
996
|
+
return { [file(allocation.page)]: page(allocation, instance.parameters.title, body2) };
|
|
997
|
+
}
|
|
998
|
+
if (block.renderer === "history-timeline") {
|
|
999
|
+
const time = entity.read.fields[instance.parameters.timeField ?? ""], title = entity.read.fields[instance.parameters.detailField ?? ""], status = entity.read.fields[instance.parameters.groupField ?? ""];
|
|
1000
|
+
if (!time || !["date", "timestamp"].includes(time.type) || !title || !status)
|
|
1001
|
+
throw new Fault(
|
|
1002
|
+
"TIMELINE_BINDING_REQUIRED",
|
|
1003
|
+
"Timeline needs a temporal field, title and status mappings.",
|
|
1004
|
+
5
|
|
1005
|
+
);
|
|
1006
|
+
const sql = `select 'EV' USER_AVATAR, apex_escape.html(${title.column}) USER_NAME, ${time.column} EVENT_DATE, apex_escape.html(${title.column}) EVENT_TITLE, apex_escape.html(${status.column}) EVENT_DESC, 'fa-history' EVENT_ICON, apex_escape.html(${status.column}) EVENT_STATUS, cast(null as varchar2(100)) EVENT_LINK, 'History' EVENT_TYPE from ${entity.read.object} where (${predicate}) order by ${time.column}, ${keys.map((k) => entity.read.fields[k].column).join(", ")}`;
|
|
1007
|
+
const names = [
|
|
1008
|
+
"USER_AVATAR",
|
|
1009
|
+
"USER_NAME",
|
|
1010
|
+
"EVENT_DATE",
|
|
1011
|
+
"EVENT_TITLE",
|
|
1012
|
+
"EVENT_DESC",
|
|
1013
|
+
"EVENT_ICON",
|
|
1014
|
+
"EVENT_STATUS",
|
|
1015
|
+
"EVENT_LINK",
|
|
1016
|
+
"EVENT_TYPE"
|
|
1017
|
+
];
|
|
1018
|
+
const cols = names.map(
|
|
1019
|
+
(name, i) => node(
|
|
1020
|
+
"column",
|
|
1021
|
+
name,
|
|
1022
|
+
`reportColumnQueryId: ${i + 1}
|
|
1023
|
+
derivedColumn: N
|
|
1024
|
+
${group("heading", `heading: ${name}`)}
|
|
1025
|
+
${group("layout", `sequence: ${(i + 1) * 10}`)}`
|
|
1026
|
+
)
|
|
1027
|
+
).join("");
|
|
1028
|
+
const timeline = node(
|
|
1029
|
+
"region",
|
|
1030
|
+
allocation.prefix + "-history",
|
|
1031
|
+
`name: ${scalar(instance.parameters.title)}
|
|
1032
|
+
type: classicReport
|
|
1033
|
+
${source(sql)}
|
|
1034
|
+
${layout(10)}
|
|
1035
|
+
${appearance("standard")}
|
|
1036
|
+
${group("componentAppearance", "template: @/timeline\ntemplateOptions: #DEFAULT#")}
|
|
1037
|
+
${cols}`
|
|
1038
|
+
);
|
|
1039
|
+
return { [file(allocation.page)]: page(allocation, instance.parameters.title, timeline) };
|
|
1040
|
+
}
|
|
1041
|
+
if (block.renderer === "master-detail") {
|
|
1042
|
+
if (keys.length !== 1 || !instance.parameters.parentField)
|
|
1043
|
+
throw new Fault(
|
|
1044
|
+
"MASTER_DETAIL_BINDING_REQUIRED",
|
|
1045
|
+
"A self-referencing parent field and scalar key are required.",
|
|
1046
|
+
5
|
|
1047
|
+
);
|
|
1048
|
+
const key = entity.read.fields[keys[0]], parent = entity.read.fields[instance.parameters.parentField];
|
|
1049
|
+
if (parent.type !== key.type)
|
|
1050
|
+
throw new Fault("MASTER_DETAIL_KEY_MISMATCH", "Parent and key types differ.", 5);
|
|
1051
|
+
const selected = `P${allocation.page}_PARENT`, master = allocation.prefix + "-master", detail = allocation.prefix + "-detail";
|
|
1052
|
+
const cols = () => fields.map(
|
|
1053
|
+
([name, field], i) => node(
|
|
1054
|
+
"column",
|
|
1055
|
+
field.column,
|
|
1056
|
+
`type: plainText
|
|
1057
|
+
${group("heading", `heading: ${name}`)}
|
|
1058
|
+
${group("layout", `sequence: ${(i + 1) * 10}`)}
|
|
1059
|
+
${group("source", `dataType: ${["integer", "decimal"].includes(field.type) ? "NUMBER" : ["date", "timestamp"].includes(field.type) ? "DATE" : "STRING"}`)}`
|
|
1060
|
+
)
|
|
1061
|
+
).join("");
|
|
1062
|
+
const list = (region2, predicateSQL, sequence, link = false) => node(
|
|
1063
|
+
"region",
|
|
1064
|
+
region2,
|
|
1065
|
+
`name: ${link ? "Master records" : "Related records"}
|
|
1066
|
+
type: interactiveReport
|
|
1067
|
+
${source(`select ${fields.map(([, f]) => f.column).join(", ")} from ${entity.read.object} where (${predicate}) and ${predicateSQL}`)}
|
|
1068
|
+
${layout(sequence)}
|
|
1069
|
+
${appearance("interactive-report")}
|
|
1070
|
+
${link ? group("link", `linkColumn: customTarget
|
|
1071
|
+
target: {
|
|
1072
|
+
page: ${allocation.page}
|
|
1073
|
+
items: {
|
|
1074
|
+
${selected}: #${key.column}#
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
linkIcon: View`) : ""}
|
|
1078
|
+
${cols()}`
|
|
1079
|
+
);
|
|
1080
|
+
const hidden = node(
|
|
1081
|
+
"pageItem",
|
|
1082
|
+
selected,
|
|
1083
|
+
`type: hidden
|
|
1084
|
+
${group("layout", `sequence: 1
|
|
1085
|
+
region: @${master}
|
|
1086
|
+
slot: regionBody`)}
|
|
1087
|
+
${group("security", "sessionStateProtection: checksumRequiredSessionLevel")}`
|
|
1088
|
+
);
|
|
1089
|
+
return {
|
|
1090
|
+
[file(allocation.page)]: page(
|
|
1091
|
+
allocation,
|
|
1092
|
+
instance.parameters.title,
|
|
1093
|
+
list(master, `${parent.column} is null`, 10, true) + hidden + list(detail, `${parent.column}=:${selected}`, 20)
|
|
1094
|
+
)
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
const region = allocation.prefix + "-records", filterItem = `P${allocation.page}_FILTER`;
|
|
1098
|
+
const where = instance.parameters.filterField ? `(${predicate}) and (${entity.read.fields[instance.parameters.filterField].column} = :${filterItem} or :${filterItem} is null)` : `(${predicate})`;
|
|
1099
|
+
const columns = fields.map(
|
|
1100
|
+
([name, field], i) => node(
|
|
1101
|
+
"column",
|
|
1102
|
+
field.column,
|
|
1103
|
+
`type: ${keys.includes(name) && writable ? "hidden" : "plainText"}
|
|
1104
|
+
${group("heading", `heading: ${scalar(name)}`)}
|
|
1105
|
+
${group("layout", `sequence: ${(i + 1) * 10}`)}
|
|
1106
|
+
${group("source", `dataType: ${["integer", "decimal"].includes(field.type) ? "NUMBER" : ["date", "timestamp"].includes(field.type) ? "DATE" : "STRING"}`)}`
|
|
1107
|
+
)
|
|
1108
|
+
).join("\n");
|
|
1109
|
+
const report = node(
|
|
1110
|
+
"region",
|
|
1111
|
+
region,
|
|
1112
|
+
[
|
|
1113
|
+
`name: ${scalar(instance.parameters.title)}`,
|
|
1114
|
+
"type: interactiveReport",
|
|
1115
|
+
source(
|
|
1116
|
+
`select ${fields.map(([, f]) => f.column).join(", ")} from ${entity.read.object} where ${where}`,
|
|
1117
|
+
instance.parameters.filterField ? filterItem : void 0
|
|
1118
|
+
),
|
|
1119
|
+
layout(10),
|
|
1120
|
+
appearance("interactive-report"),
|
|
1121
|
+
group("advanced", `htmlDomId: ${allocation.prefix}_records`),
|
|
1122
|
+
writable && instance.parameters.editEnabled ? group(
|
|
1123
|
+
"link",
|
|
1124
|
+
`linkColumn: customTarget
|
|
1125
|
+
target: {
|
|
1126
|
+
page: ${allocation.dialog}
|
|
1127
|
+
items: {
|
|
1128
|
+
P${allocation.dialog}_${keys[0].toUpperCase()}: #${entity.read.fields[keys[0]].column}#
|
|
1129
|
+
}
|
|
1130
|
+
clearCache: ${allocation.dialog}
|
|
1131
|
+
}
|
|
1132
|
+
linkIcon: <span class="fa fa-edit" aria-label="Edit"></span>`
|
|
1133
|
+
) : "",
|
|
1134
|
+
columns
|
|
1135
|
+
].filter(Boolean).join("\n")
|
|
1136
|
+
);
|
|
1137
|
+
let body = report;
|
|
1138
|
+
if (instance.parameters.filterField) {
|
|
1139
|
+
const filter = instance.parameters.filterField;
|
|
1140
|
+
body += node(
|
|
1141
|
+
"pageItem",
|
|
1142
|
+
filterItem,
|
|
1143
|
+
`type: textField
|
|
1144
|
+
${group("label", `label: ${filter}`)}
|
|
1145
|
+
${group("layout", `sequence: 5
|
|
1146
|
+
region: @${region}
|
|
1147
|
+
slot: regionBody`)}
|
|
1148
|
+
${group("appearance", "template: @/optional-floating\ntemplateOptions: #DEFAULT#")}`
|
|
1149
|
+
);
|
|
1150
|
+
body += dynamic(
|
|
1151
|
+
allocation.prefix + "-filter",
|
|
1152
|
+
"change",
|
|
1153
|
+
`selectionType: items
|
|
1154
|
+
items: ${filterItem}`,
|
|
1155
|
+
refresh("refresh", region, 10)
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
if (writable && instance.parameters.createEnabled)
|
|
1159
|
+
body += button(
|
|
1160
|
+
allocation.prefix + "-create",
|
|
1161
|
+
"Create",
|
|
1162
|
+
region,
|
|
1163
|
+
`action: redirectThisApp
|
|
1164
|
+
target: {
|
|
1165
|
+
page: ${allocation.dialog}
|
|
1166
|
+
clearCache: ${allocation.dialog}
|
|
1167
|
+
}`
|
|
1168
|
+
);
|
|
1169
|
+
for (const summary of summaries) body += summaryRegion(blueprint, summary.instance, summary.allocation);
|
|
1170
|
+
if (writable) {
|
|
1171
|
+
const targets = [
|
|
1172
|
+
allocation.prefix + "_records",
|
|
1173
|
+
...summaries.map((s) => s.allocation.prefix + "_summary")
|
|
1174
|
+
];
|
|
1175
|
+
const refreshes = jsAction(
|
|
1176
|
+
"refresh-bound-regions",
|
|
1177
|
+
`var e=this.data; if(!e||e.originInstance!==${JSON.stringify(id)}||e.entityRef!==${JSON.stringify(binding.entityRef)}||!e.recordKey||!e.recordVersion||!e.correlationId||!['create','edit'].includes(e.operation)) return; var host=document.getElementById(${JSON.stringify(allocation.prefix + "_records")}); if(host.dataset.composerCorrelation===e.correlationId) return; host.dataset.composerCorrelation=e.correlationId; ${JSON.stringify(targets)}.forEach(function(id){var region=apex.region(id); if(region) region.refresh();});`
|
|
1178
|
+
);
|
|
1179
|
+
body += dynamic(
|
|
1180
|
+
allocation.prefix + "-saved",
|
|
1181
|
+
"apexafterclosedialog",
|
|
1182
|
+
`selectionType: region
|
|
1183
|
+
region: @${region}`,
|
|
1184
|
+
refreshes
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
const result = {
|
|
1188
|
+
[file(allocation.page)]: page(allocation, instance.parameters.title, body)
|
|
1189
|
+
};
|
|
1190
|
+
if (!writable || !command || !allocation.dialog) return result;
|
|
1191
|
+
const dialog = allocation.dialog, version = entity.capabilities.optimisticLock.field;
|
|
1192
|
+
const itemName = (field) => `P${dialog}_${field.toUpperCase()}`;
|
|
1193
|
+
const numberFormat = "99999999999999999999999999999999999999D99999999999999999999999999999999999999";
|
|
1194
|
+
const inputExpression = (field) => {
|
|
1195
|
+
const spec = entity.read.fields[field], value = ":" + itemName(field);
|
|
1196
|
+
return ["integer", "decimal"].includes(spec.type) ? `to_number(${value},'${numberFormat}','NLS_NUMERIC_CHARACTERS=''.,''')` : spec.type === "date" ? `to_date(${value},'FXYYYY-MM-DD')` : spec.type === "timestamp" ? `to_timestamp(${value},'FXYYYY-MM-DD"T"HH24:MI:SS.FF6')` : value;
|
|
1197
|
+
};
|
|
1198
|
+
const readExpression = (field) => {
|
|
1199
|
+
const spec = entity.read.fields[field];
|
|
1200
|
+
return ["integer", "decimal"].includes(spec.type) ? `to_char(${spec.column},'TM9','NLS_NUMERIC_CHARACTERS=''.,''')` : spec.type === "date" ? `to_char(${spec.column},'YYYY-MM-DD')` : spec.type === "timestamp" ? `to_char(${spec.column},'YYYY-MM-DD"T"HH24:MI:SS.FF6')` : spec.column;
|
|
1201
|
+
};
|
|
1202
|
+
const validation = instance.parameters.editableFields.map((field) => {
|
|
1203
|
+
const spec = entity.read.fields[field], value = ":" + itemName(field), checks = [];
|
|
1204
|
+
if (!spec.nullable) checks.push(`${value} is null`);
|
|
1205
|
+
if (spec.maxLength) checks.push(`length(${value})>${spec.maxLength}`);
|
|
1206
|
+
if (spec.enum?.length) checks.push(`${value} not in (${spec.enum.map(sqlLiteral).join(", ")})`);
|
|
1207
|
+
if (["integer", "decimal"].includes(spec.type))
|
|
1208
|
+
checks.push(
|
|
1209
|
+
`${value} is not null and not regexp_like(${value},'${spec.type === "integer" ? "^[+-]?[0-9]+$" : "^[+-]?[0-9]+([.][0-9]+)?$"}')`
|
|
1210
|
+
);
|
|
1211
|
+
return checks.length ? `if ${checks.map((c) => "(" + c + ")").join(" or ")} then raise_application_error(-20002,'Invalid ${field}'); end if;` : "";
|
|
1212
|
+
}).join("\n ");
|
|
1213
|
+
let form = node(
|
|
1214
|
+
"region",
|
|
1215
|
+
"form",
|
|
1216
|
+
`name: ${scalar(instance.parameters.title)}
|
|
1217
|
+
type: staticContent
|
|
1218
|
+
${layout(10, "contentBody")}
|
|
1219
|
+
${appearance("standard")}`
|
|
1220
|
+
);
|
|
1221
|
+
form += node(
|
|
1222
|
+
"region",
|
|
1223
|
+
"buttons",
|
|
1224
|
+
`name: Actions
|
|
1225
|
+
type: staticContent
|
|
1226
|
+
${layout(20, "dialogFooter")}
|
|
1227
|
+
${appearance("buttons-container")}`
|
|
1228
|
+
);
|
|
1229
|
+
for (const [field, spec] of fields)
|
|
1230
|
+
if (instance.parameters.editableFields.includes(field) || keys.includes(field) || field === version)
|
|
1231
|
+
form += item(
|
|
1232
|
+
itemName(field),
|
|
1233
|
+
field,
|
|
1234
|
+
fields.findIndex(([n]) => n === field) * 10 + 10,
|
|
1235
|
+
!instance.parameters.editableFields.includes(field),
|
|
1236
|
+
!spec.nullable
|
|
1237
|
+
);
|
|
1238
|
+
form += button("save", "Save", "buttons", "action: definedByDynamicAction");
|
|
1239
|
+
form += button("cancel", "Cancel", "buttons", "action: definedByDynamicAction");
|
|
1240
|
+
form += dynamic(
|
|
1241
|
+
"cancel-dialog",
|
|
1242
|
+
"click",
|
|
1243
|
+
"selectionType: button\nbutton: @cancel",
|
|
1244
|
+
node(
|
|
1245
|
+
"action",
|
|
1246
|
+
"cancel",
|
|
1247
|
+
"action: cancelDialog\n" + group("execution", "sequence: 10\nfireOnInit: false")
|
|
1248
|
+
)
|
|
1249
|
+
);
|
|
1250
|
+
const mappedFields = fields.filter(
|
|
1251
|
+
([field]) => instance.parameters.editableFields.includes(field) || keys.includes(field) || field === version
|
|
1252
|
+
);
|
|
1253
|
+
form += process2(
|
|
1254
|
+
allocation.prefix + "-read",
|
|
1255
|
+
"beforeHeader",
|
|
1256
|
+
`begin
|
|
1257
|
+
if :${itemName(keys[0])} is not null then
|
|
1258
|
+
select ${mappedFields.map(([f]) => readExpression(f)).join(", ")} into ${mappedFields.map(([f]) => ":" + itemName(f)).join(", ")} from ${entity.read.object} where ${entity.read.fields[keys[0]].column} = :${itemName(keys[0])} and (${predicate});
|
|
1259
|
+
end if;
|
|
1260
|
+
end;`
|
|
1261
|
+
);
|
|
1262
|
+
const variables = /* @__PURE__ */ new Map();
|
|
1263
|
+
for (const [argument, mapping] of Object.entries(command.inputs).sort(([a], [b]) => a < b ? -1 : 1))
|
|
1264
|
+
variables.set(argument, mapping.mode === "in-out" ? "l_key" : inputExpression(mapping.from.slice(7)));
|
|
1265
|
+
variables.set(command.outputs.recordKey.from, "l_key");
|
|
1266
|
+
variables.set(command.outputs.recordVersion.from, "l_version");
|
|
1267
|
+
const saveName = allocation.prefix + "_SAVE";
|
|
1268
|
+
const server = `declare
|
|
1269
|
+
l_key ${entity.read.object}.${entity.read.fields[keys[0]].column}%type;
|
|
1270
|
+
l_authorized boolean;
|
|
1271
|
+
l_visible pls_integer;
|
|
1272
|
+
l_version ${entity.read.object}.${entity.read.fields[version].column}%type;
|
|
1273
|
+
begin
|
|
1274
|
+
savepoint composer_save;
|
|
1275
|
+
l_authorized := (${binding.writeExpression});
|
|
1276
|
+
if l_authorized is null or not l_authorized or not apex_authentication.is_authenticated then raise_application_error(-20001, 'Authorization denied'); end if;
|
|
1277
|
+
l_key := ${inputExpression(keys[0])};
|
|
1278
|
+
if apex_application.g_x01 = 'create' then
|
|
1279
|
+
if ${instance.parameters.createEnabled ? "false" : "true"} or l_key is not null or :${itemName(version)} is not null then raise_application_error(-20002, 'Invalid create draft'); end if;
|
|
1280
|
+
elsif apex_application.g_x01 = 'edit' then
|
|
1281
|
+
if ${instance.parameters.editEnabled ? "false" : "true"} or l_key is null or :${itemName(version)} is null then raise_application_error(-20002, 'Invalid edit draft'); end if;
|
|
1282
|
+
select count(*) into l_visible from ${entity.read.object} where ${entity.read.fields[keys[0]].column} = l_key and (${predicate}) and rownum = 1;
|
|
1283
|
+
if l_visible = 0 then raise_application_error(-20001, 'Authorization denied'); end if;
|
|
1284
|
+
else raise_application_error(-20002, 'Invalid operation'); end if;
|
|
1285
|
+
${validation}
|
|
1286
|
+
${instance.extensions.beforeSaveValidation ?? ""}
|
|
1287
|
+
${command.package}.${command.procedure}(${[...variables].map(([argument, value]) => `${argument} => ${value}`).join(", ")});
|
|
1288
|
+
if l_key is null or l_version is null then raise_application_error(-20004, 'API output contract violated'); end if;
|
|
1289
|
+
${instance.extensions.afterSaveNotification ?? ""}
|
|
1290
|
+
apex_json.open_object; apex_json.write('ok',true); apex_json.write('recordKey',${["integer", "decimal"].includes(entity.read.fields[keys[0]].type) ? "to_char(l_key,'TM9','NLS_NUMERIC_CHARACTERS=''.,''')" : "l_key"}); apex_json.write('recordVersion',to_char(l_version,'TM9','NLS_NUMERIC_CHARACTERS=''.,''')); apex_json.close_object;
|
|
1291
|
+
exception when others then
|
|
1292
|
+
rollback to composer_save;
|
|
1293
|
+
apex_json.open_object; apex_json.write('ok',false); apex_json.write('code',case sqlcode when -20001 then 'authorization' when -20002 then 'validation' when -20003 then 'conflict' else 'server-error' end); apex_json.write('message','Save failed. Review fields and reload after a conflict.'); apex_json.close_object;
|
|
1294
|
+
end;`;
|
|
1295
|
+
form += process2(saveName, "ajaxCallback", server);
|
|
1296
|
+
const pageItems = mappedFields.map(([f]) => "#" + itemName(f)).join(",");
|
|
1297
|
+
const correlation = "(window.crypto && window.crypto.randomUUID ? window.crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2))";
|
|
1298
|
+
const js = `var button = this.triggeringElement; if (button.disabled) return; button.disabled = true; var saved = false;
|
|
1299
|
+
var keyItem = apex.item(${JSON.stringify(itemName(keys[0]))}), operation = keyItem.getValue() ? 'edit' : 'create';
|
|
1300
|
+
apex.server.process(${JSON.stringify(saveName)}, {x01: operation, pageItems: ${JSON.stringify(pageItems)}}, {dataType: 'json', success: function(data) { if (data.ok) { saved = true; try { keyItem.setValue(data.recordKey); apex.item(${JSON.stringify(itemName(version))}).setValue(data.recordVersion); } catch (e) {} try { apex.navigation.dialog.close(true, {entityRef: ${JSON.stringify(binding.entityRef)}, recordKey: data.recordKey, recordVersion: data.recordVersion, operation: operation, originInstance: ${JSON.stringify(id)}, correlationId: ${correlation}}); } catch (e) { apex.message.showErrors([{type:'error',location:'page',message:'Saved. Close this dialog and refresh the report.',unsafe:false}]); } } else { apex.message.showErrors([{type:'error',location:'page',message:data.message,unsafe:false}]); } }, error: function() {apex.message.showErrors([{type:'error',location:'page',message:'Save request failed.',unsafe:false}]);}, complete: function() { if (!saved) button.disabled = false; } });`;
|
|
1301
|
+
form += dynamic("save-dialog", "click", "selectionType: button\nbutton: @save", jsAction("save-api", js));
|
|
1302
|
+
result[file(dialog)] = page(allocation, instance.parameters.title, form, true);
|
|
1303
|
+
return result;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// packages/core/src/composer/planner.ts
|
|
1307
|
+
var extensionUnsafe = () => new Fault(
|
|
1308
|
+
"EXTENSION_UNSAFE",
|
|
1309
|
+
"Extension code must preserve caller-owned transactions and literal boundaries.",
|
|
1310
|
+
5
|
|
1311
|
+
);
|
|
1312
|
+
function extensionCode(source2) {
|
|
1313
|
+
if (/```|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]|\b[nN]?[qQ]'/.test(source2)) throw extensionUnsafe();
|
|
1314
|
+
let code2 = "", at = 0;
|
|
1315
|
+
while (at < source2.length) {
|
|
1316
|
+
if (source2.startsWith("--", at)) {
|
|
1317
|
+
const end = source2.indexOf("\n", at);
|
|
1318
|
+
at = end < 0 ? source2.length : end;
|
|
1319
|
+
code2 += " ";
|
|
1320
|
+
} else if (source2.startsWith("/*", at)) {
|
|
1321
|
+
const end = source2.indexOf("*/", at + 2);
|
|
1322
|
+
if (end < 0) throw extensionUnsafe();
|
|
1323
|
+
at = end + 2;
|
|
1324
|
+
code2 += " ";
|
|
1325
|
+
} else if (source2[at] === "'") {
|
|
1326
|
+
const end = source2.slice(at + 1).search(/'(?!')/);
|
|
1327
|
+
if (end < 0) throw extensionUnsafe();
|
|
1328
|
+
at += end + 2;
|
|
1329
|
+
code2 += "''";
|
|
1330
|
+
} else if (source2[at] === '"') {
|
|
1331
|
+
const end = source2.indexOf('"', at + 1);
|
|
1332
|
+
if (end < 0) throw extensionUnsafe();
|
|
1333
|
+
code2 += " " + source2.slice(at + 1, end) + " ";
|
|
1334
|
+
at = end + 1;
|
|
1335
|
+
} else code2 += source2[at++];
|
|
1336
|
+
}
|
|
1337
|
+
const normalized = code2.replace(/\s+/g, " ");
|
|
1338
|
+
if (/\b(?:commit|rollback|savepoint|grant|revoke|host|connect|autonomous_transaction)\b|\bexecute\s+immediate\b|\b(?:dbms_sql|dbms_sys_sql|dbms_job|dbms_scheduler|dbms_pipe|dbms_java|dbms_aq\w*|utl_\w+)\b|\bsys\s*\./i.test(
|
|
1339
|
+
normalized
|
|
1340
|
+
))
|
|
1341
|
+
throw extensionUnsafe();
|
|
1342
|
+
return source2;
|
|
1343
|
+
}
|
|
1344
|
+
function derivedNames(prefix) {
|
|
1345
|
+
return [
|
|
1346
|
+
prefix,
|
|
1347
|
+
prefix + "_EDIT",
|
|
1348
|
+
prefix + "_SAVE",
|
|
1349
|
+
prefix + "_records",
|
|
1350
|
+
prefix + "_summary",
|
|
1351
|
+
...["records", "summary", "history", "master", "detail", "filter", "create", "saved", "read"].map(
|
|
1352
|
+
(suffix) => prefix + "-" + suffix
|
|
1353
|
+
)
|
|
1354
|
+
].map((name) => name.toUpperCase());
|
|
1355
|
+
}
|
|
1356
|
+
function sourceIdentities(sources) {
|
|
1357
|
+
const names = new Set(inventorySymbols(sources).symbols);
|
|
1358
|
+
for (const source2 of Object.values(sources))
|
|
1359
|
+
for (const match2 of source2.matchAll(
|
|
1360
|
+
/^[ \t]*(?:alias|htmlDomId|staticId|buttonName|name):[ \t]*([^\s]+)[ \t]*$/gm
|
|
1361
|
+
))
|
|
1362
|
+
names.add(match2[1].toUpperCase());
|
|
1363
|
+
return names;
|
|
1364
|
+
}
|
|
1365
|
+
var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1366
|
+
function pageReference(source2, page2, alias) {
|
|
1367
|
+
const target = alias ? `(?:${page2}|${escapeRegExp(alias)})` : page2, end = "(?![A-Za-z0-9_$#-])";
|
|
1368
|
+
if (new RegExp("\\bpage:\\s*" + target + end, "i").test(source2) || source2.includes(":" + page2 + ":") || new RegExp(`f\\?p=[^:\\s'"]*:${target}${end}`, "i").test(source2) || new RegExp(`\\bp_page\\s*=>\\s*'?${target}${end}`, "i").test(source2) || alias && new RegExp(`(?<![A-Za-z0-9_$#-])${escapeRegExp(alias)}${end}`, "i").test(source2))
|
|
1369
|
+
return "literal";
|
|
1370
|
+
if (/\bp_page\s*=>(?!\s*(?:'[A-Za-z0-9_$#]*'|\d+\b))/i.test(source2) || /\bapex_page\.get_url\s*\((?!\s*(?:p_|\)))/i.test(source2) || /f\?p=[^:\s'"]*:(?:&(?!APP_PAGE_ID\.)|#|'\s*\|\|)/i.test(source2))
|
|
1371
|
+
return "dynamic";
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
async function snapshot(ctx, blueprintPath, options = {}) {
|
|
1375
|
+
const root = await safePath(ctx.root, ctx.config.application.sourceDir), sourceInventory = await inventory(root);
|
|
1376
|
+
const sources = {};
|
|
1377
|
+
for (const file of Object.keys(sourceInventory).filter((file2) => file2.endsWith(".apx"))) {
|
|
1378
|
+
const bytes = await readFile(await safePath(root, file)), text = bytes.toString("utf8");
|
|
1379
|
+
if (!Buffer.from(text).equals(bytes))
|
|
1380
|
+
throw new Fault("TRANSFORM_UNSUPPORTED", "Source must be valid UTF-8.", 5);
|
|
1381
|
+
sources[file] = text;
|
|
1382
|
+
}
|
|
1383
|
+
const stateFile = await safePath(ctx.root, ".apexrest-composer/state.json");
|
|
1384
|
+
const state = await exists(stateFile) ? await readDocument(ctx.root, ".apexrest-composer/state.json", stateSchema) : null;
|
|
1385
|
+
const bases = {};
|
|
1386
|
+
const baseRoot = await safePath(ctx.root, ".apexrest-composer/bases");
|
|
1387
|
+
if (await exists(baseRoot))
|
|
1388
|
+
for (const entry2 of await readdir(baseRoot)) {
|
|
1389
|
+
if (!/^[a-f0-9]{64}\.apx$/.test(entry2))
|
|
1390
|
+
throw new Fault("GENERATION_BASE_CORRUPT", "Unexpected generated base entry.", 5);
|
|
1391
|
+
const bytes = await readFile(await safePath(baseRoot, entry2)), digest3 = entry2.slice(0, -4);
|
|
1392
|
+
if (hash(bytes) !== digest3)
|
|
1393
|
+
throw new Fault("GENERATION_BASE_CORRUPT", "Generated base integrity failed.", 5);
|
|
1394
|
+
bases[digest3] = bytes.toString("utf8");
|
|
1395
|
+
}
|
|
1396
|
+
if (state) {
|
|
1397
|
+
for (const owner of Object.values(state.owners))
|
|
1398
|
+
for (const digest3 of Object.values(owner.bases))
|
|
1399
|
+
if (!(digest3 in bases))
|
|
1400
|
+
throw new Fault("GENERATION_BASE_MISSING", "Previous generated base is unavailable.", 5);
|
|
1401
|
+
}
|
|
1402
|
+
const blueprint = await readDocument(ctx.root, blueprintPath, blueprintSchema);
|
|
1403
|
+
const mode = options.mode ?? "offline";
|
|
1404
|
+
if (mode === "connected" && (!options.environment || !options.metadata))
|
|
1405
|
+
throw new Fault(
|
|
1406
|
+
"ENVIRONMENT_REQUIRED",
|
|
1407
|
+
"Connected planning requires explicit environment and metadata.",
|
|
1408
|
+
2
|
|
1409
|
+
);
|
|
1410
|
+
if (options.environment && blueprint.application.environment && options.environment !== blueprint.application.environment)
|
|
1411
|
+
throw new Fault("ENVIRONMENT_MISMATCH", "Blueprint and requested environment differ.", 5);
|
|
1412
|
+
return {
|
|
1413
|
+
blueprint,
|
|
1414
|
+
blueprintPath,
|
|
1415
|
+
catalog: await loadCatalog(ctx.root),
|
|
1416
|
+
state,
|
|
1417
|
+
sources,
|
|
1418
|
+
sourceInventory,
|
|
1419
|
+
bases,
|
|
1420
|
+
configurationDigest: semanticDigest(ctx.config),
|
|
1421
|
+
toolchainDigest: hash(await readFile(await safePath(ctx.root, ctx.config.toolchain.lockFile))),
|
|
1422
|
+
sourceDir: ctx.config.application.sourceDir,
|
|
1423
|
+
projectId: ctx.config.projectId,
|
|
1424
|
+
mode,
|
|
1425
|
+
environment: options.environment ?? null,
|
|
1426
|
+
metadata: options.metadata ?? null,
|
|
1427
|
+
validation: options.validation ?? "compiler"
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
function planComposition(input) {
|
|
1431
|
+
const { blueprint, state, catalog: catalog3 } = input;
|
|
1432
|
+
const diagnostics = [], operations = [];
|
|
1433
|
+
const blueprintDigest = semanticDigest(blueprint);
|
|
1434
|
+
const plan = {
|
|
1435
|
+
schemaVersion: 1,
|
|
1436
|
+
generatorVersion: "1",
|
|
1437
|
+
kind: "composition",
|
|
1438
|
+
status: "blocked",
|
|
1439
|
+
projectId: input.projectId,
|
|
1440
|
+
blueprintPath: input.blueprintPath,
|
|
1441
|
+
blueprintDigest,
|
|
1442
|
+
catalogDigest: catalog3.digest,
|
|
1443
|
+
configurationDigest: input.configurationDigest,
|
|
1444
|
+
toolchainDigest: input.toolchainDigest,
|
|
1445
|
+
sourceInventory: input.sourceInventory,
|
|
1446
|
+
stateDigest: state ? semanticDigest(state) : null,
|
|
1447
|
+
validation: input.validation,
|
|
1448
|
+
mode: input.mode,
|
|
1449
|
+
contextDigest: semanticDigest(
|
|
1450
|
+
input.metadata ?? { assumptions: blueprint.application.compatibilityProfile }
|
|
1451
|
+
),
|
|
1452
|
+
environment: input.environment,
|
|
1453
|
+
review: {
|
|
1454
|
+
entities: blueprint.entities,
|
|
1455
|
+
commands: blueprint.commands,
|
|
1456
|
+
contracts: blueprint.contracts,
|
|
1457
|
+
packages: {}
|
|
1458
|
+
},
|
|
1459
|
+
allocations: {},
|
|
1460
|
+
operations,
|
|
1461
|
+
state: null,
|
|
1462
|
+
lock: null,
|
|
1463
|
+
diagnostics,
|
|
1464
|
+
digest: "0".repeat(64)
|
|
1465
|
+
};
|
|
1466
|
+
try {
|
|
1467
|
+
let visit = function(id) {
|
|
1468
|
+
if (visiting.has(id)) throw new Fault("INTERACTION_CYCLE", "Runtime event cycles are unsupported.", 5);
|
|
1469
|
+
if (visited.has(id)) return;
|
|
1470
|
+
visiting.add(id);
|
|
1471
|
+
for (const target of graph.get(id) ?? []) visit(target);
|
|
1472
|
+
visiting.delete(id);
|
|
1473
|
+
visited.add(id);
|
|
1474
|
+
};
|
|
1475
|
+
if (Object.keys(blueprint.blocks).length > 256)
|
|
1476
|
+
throw new Fault("INSTANCE_LIMIT", "At most 256 instances are supported.", 2);
|
|
1477
|
+
const packages = resolvePackages(
|
|
1478
|
+
catalog3,
|
|
1479
|
+
Object.values(blueprint.blocks).map((b) => b.use),
|
|
1480
|
+
blueprint.application.compatibilityProfile
|
|
1481
|
+
);
|
|
1482
|
+
plan.review.packages = Object.fromEntries(
|
|
1483
|
+
[...packages].map(([id, p]) => [
|
|
1484
|
+
id,
|
|
1485
|
+
{
|
|
1486
|
+
digest: p.digest,
|
|
1487
|
+
origin: p.manifest.origin,
|
|
1488
|
+
license: p.manifest.license,
|
|
1489
|
+
effects: p.manifest.effects,
|
|
1490
|
+
dependencies: p.manifest.requires.blocks
|
|
1491
|
+
}
|
|
1492
|
+
])
|
|
1493
|
+
);
|
|
1494
|
+
for (const pkg of packages.values())
|
|
1495
|
+
if (pkg.manifest.status === "deprecated")
|
|
1496
|
+
diagnostics.push({
|
|
1497
|
+
code: "BLOCK_DEPRECATED",
|
|
1498
|
+
severity: "warning",
|
|
1499
|
+
message: "Selected exact block is deprecated; review replacement separately."
|
|
1500
|
+
});
|
|
1501
|
+
const symbols = inventorySymbols(input.sources), used = new Set(symbols.pages), prefixes = /* @__PURE__ */ new Set(), ownedFiles = new Set(Object.values(state?.owners ?? {}).flatMap((owner) => Object.keys(owner.files))), existing = sourceIdentities(
|
|
1502
|
+
Object.fromEntries(Object.entries(input.sources).filter(([file]) => !ownedFiles.has(file)))
|
|
1503
|
+
);
|
|
1504
|
+
const allocated = {};
|
|
1505
|
+
const nextPage = () => {
|
|
1506
|
+
for (let page2 = 100; page2 < 9999; page2++)
|
|
1507
|
+
if (!used.has(page2)) {
|
|
1508
|
+
used.add(page2);
|
|
1509
|
+
return page2;
|
|
1510
|
+
}
|
|
1511
|
+
throw new Fault("ALLOCATION_EXHAUSTED", "No available page identity.", 5);
|
|
1512
|
+
};
|
|
1513
|
+
for (const [id, instance] of Object.entries(blueprint.blocks).sort(([a], [b]) => a < b ? -1 : 1)) {
|
|
1514
|
+
const binding = bind(blueprint, instance, input.metadata ?? void 0);
|
|
1515
|
+
if (Object.keys(instance.extensions).length && instance.ownership !== "extended")
|
|
1516
|
+
throw new Fault("EXTENSION_MODE_REQUIRED", "Extension hooks require explicit extended ownership.", 5);
|
|
1517
|
+
for (const source2 of Object.values(instance.extensions)) extensionCode(source2);
|
|
1518
|
+
const previous = state?.owners[id], prefix = previous?.allocation.prefix ?? "cmp_" + id.toLowerCase().replaceAll("-", "_").slice(0, 24) + "_" + hash(id).slice(0, 8);
|
|
1519
|
+
const names = derivedNames(prefix);
|
|
1520
|
+
if (names.some(
|
|
1521
|
+
(name) => prefixes.has(name) || existing.has(name) || !previous && symbols.symbols.has(name)
|
|
1522
|
+
))
|
|
1523
|
+
throw new Fault("SYMBOL_COLLISION", "A block namespace collides with existing source.", 5);
|
|
1524
|
+
for (const name of names) prefixes.add(name);
|
|
1525
|
+
if (previous)
|
|
1526
|
+
for (const file of Object.keys(previous.files)) {
|
|
1527
|
+
if (!input.sources[file])
|
|
1528
|
+
throw new Fault(
|
|
1529
|
+
"OWNED_SOURCE_MISSING",
|
|
1530
|
+
"Owned source is missing; explicit adoption is required.",
|
|
1531
|
+
5
|
|
1532
|
+
);
|
|
1533
|
+
const page2 = declarations(input.sources[file]).find((n) => n.kind === "page" && n.depth === 0);
|
|
1534
|
+
if (!page2 || ![previous.allocation.page, previous.allocation.dialog].includes(Number(page2.key)))
|
|
1535
|
+
throw new Fault("OWNED_IDENTITY_CHANGED", "Owned page identity changed.", 5);
|
|
1536
|
+
}
|
|
1537
|
+
allocated[id] = {
|
|
1538
|
+
page: previous?.allocation.page ?? nextPage(),
|
|
1539
|
+
dialog: binding.writable ? previous?.allocation.dialog ?? nextPage() : null,
|
|
1540
|
+
prefix
|
|
1541
|
+
};
|
|
1542
|
+
const pkg = packages.get(instance.use);
|
|
1543
|
+
if (binding.writable && pkg.manifest.renderer !== "report-dialog")
|
|
1544
|
+
throw new Fault(
|
|
1545
|
+
"BLOCK_WRITE_UNSUPPORTED",
|
|
1546
|
+
"Only the report-dialog adapter implements create/edit commands.",
|
|
1547
|
+
5
|
|
1548
|
+
);
|
|
1549
|
+
const providers = /* @__PURE__ */ new Set([
|
|
1550
|
+
"key",
|
|
1551
|
+
"readAuthorization",
|
|
1552
|
+
...binding.writable ? ["optimisticLock", "writeAuthorization"] : []
|
|
1553
|
+
]);
|
|
1554
|
+
for (const capability of pkg.manifest.requires.capabilities)
|
|
1555
|
+
if (!providers.has(capability) && !(pkg.manifest.renderer === "report-dialog" && !binding.writable && ["optimisticLock", "writeAuthorization"].includes(capability)))
|
|
1556
|
+
throw new Fault("CAPABILITY_MISSING", "A required block capability has no reviewed provider.", 5);
|
|
1557
|
+
}
|
|
1558
|
+
const hosts = /* @__PURE__ */ new Map(), graph = /* @__PURE__ */ new Map();
|
|
1559
|
+
for (const connection of blueprint.connections) {
|
|
1560
|
+
const from = connection.from.match(/^([\w-]+)\.events\.saved$/), to = connection.to.match(/^([\w-]+)\.actions\.refresh$/);
|
|
1561
|
+
if (!from || !to || !blueprint.blocks[from[1]] || !blueprint.blocks[to[1]])
|
|
1562
|
+
throw new Fault("CONNECTION_INVALID", "Unknown or unsupported event/action port.", 5);
|
|
1563
|
+
const publisher = blueprint.blocks[from[1]], consumer = blueprint.blocks[to[1]];
|
|
1564
|
+
if (!bind(blueprint, publisher).writable || packages.get(publisher.use).manifest.renderer !== "report-dialog" || packages.get(consumer.use).manifest.renderer !== "status-summary" || publisher.bindings.records !== consumer.bindings.records)
|
|
1565
|
+
throw new Fault(
|
|
1566
|
+
"CONNECTION_CONTRACT_MISMATCH",
|
|
1567
|
+
"Saved/refresh connections require a writable report and summary of the same row scope.",
|
|
1568
|
+
5
|
|
1569
|
+
);
|
|
1570
|
+
if (hosts.has(to[1]))
|
|
1571
|
+
throw new Fault("CONNECTION_DUPLICATE", "A summary can have one explicit host.", 5);
|
|
1572
|
+
hosts.set(to[1], from[1]);
|
|
1573
|
+
graph.set(from[1], [...graph.get(from[1]) ?? [], to[1]]);
|
|
1574
|
+
}
|
|
1575
|
+
const visiting = /* @__PURE__ */ new Set(), visited = /* @__PURE__ */ new Set();
|
|
1576
|
+
for (const id of graph.keys()) visit(id);
|
|
1577
|
+
for (const [id, previous] of Object.entries(state?.owners ?? {})) {
|
|
1578
|
+
for (const host of previous.consumers) {
|
|
1579
|
+
const childDetached = blueprint.blocks[id]?.ownership === "detached" || previous.mode === "detached";
|
|
1580
|
+
const hostDetached = blueprint.blocks[host]?.ownership === "detached" || state?.owners[host]?.mode === "detached";
|
|
1581
|
+
if (childDetached !== hostDetached)
|
|
1582
|
+
throw new Fault(
|
|
1583
|
+
"SHARED_OWNERSHIP_DETACH_REQUIRED",
|
|
1584
|
+
"Detach the hosted summary and its page owner together to preserve shared source.",
|
|
1585
|
+
5
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
for (const [child, host] of hosts)
|
|
1590
|
+
if (blueprint.blocks[child].ownership === "detached" || blueprint.blocks[host].ownership === "detached")
|
|
1591
|
+
throw new Fault(
|
|
1592
|
+
"DETACHED_CONNECTION",
|
|
1593
|
+
"Remove managed connections when detaching both connected instances.",
|
|
1594
|
+
5
|
|
1595
|
+
);
|
|
1596
|
+
for (const [id, previous] of Object.entries(state?.owners ?? {}).sort(([a], [b]) => a < b ? -1 : 1))
|
|
1597
|
+
if (previous.consumers.length && blueprint.blocks[id] && blueprint.blocks[id].ownership !== "detached" && !hosts.has(id))
|
|
1598
|
+
allocated[id].page = nextPage();
|
|
1599
|
+
for (const [child, host] of hosts) allocated[child].page = allocated[host].page;
|
|
1600
|
+
plan.allocations = allocated;
|
|
1601
|
+
const desired = {}, owners = {};
|
|
1602
|
+
for (const [id, instance] of Object.entries(blueprint.blocks).sort(([a], [b]) => a < b ? -1 : 1)) {
|
|
1603
|
+
const pkg = packages.get(instance.use), previous = state?.owners[id];
|
|
1604
|
+
if (instance.ownership === "detached") {
|
|
1605
|
+
if (!previous)
|
|
1606
|
+
throw new Fault("DETACH_UNOWNED", "Only an existing owned instance can be detached.", 5);
|
|
1607
|
+
owners[id] = { ...previous, mode: "detached" };
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (previous?.mode === "detached")
|
|
1611
|
+
throw new Fault(
|
|
1612
|
+
"REATTACH_REQUIRES_ADOPTION",
|
|
1613
|
+
"Detached instances need explicit reviewed adoption.",
|
|
1614
|
+
5
|
|
1615
|
+
);
|
|
1616
|
+
const summaries = [...hosts].filter(([, host]) => host === id).map(([child]) => ({ instance: blueprint.blocks[child], allocation: allocated[child] }));
|
|
1617
|
+
const files = hosts.has(id) ? {} : render(blueprint, id, instance, pkg.manifest, allocated[id], summaries);
|
|
1618
|
+
for (const [file, source2] of Object.entries(files)) {
|
|
1619
|
+
if (desired[file])
|
|
1620
|
+
throw new Fault("OWNERSHIP_COLLISION", "Two instances own the same source file.", 5);
|
|
1621
|
+
desired[file] = source2;
|
|
1622
|
+
}
|
|
1623
|
+
owners[id] = {
|
|
1624
|
+
instanceId: id,
|
|
1625
|
+
blockId: pkg.manifest.id,
|
|
1626
|
+
version: pkg.manifest.version,
|
|
1627
|
+
mode: instance.ownership,
|
|
1628
|
+
allocation: allocated[id],
|
|
1629
|
+
files: {},
|
|
1630
|
+
bases: {},
|
|
1631
|
+
consumers: hosts.has(id) ? [hosts.get(id)] : [],
|
|
1632
|
+
provenance: semanticDigest({
|
|
1633
|
+
package: pkg.digest,
|
|
1634
|
+
instance,
|
|
1635
|
+
bindings: blueprint.entities,
|
|
1636
|
+
commands: blueprint.commands,
|
|
1637
|
+
contracts: blueprint.contracts
|
|
1638
|
+
})
|
|
1639
|
+
};
|
|
1640
|
+
for (const [file, source2] of Object.entries(files)) {
|
|
1641
|
+
owners[id].files[file] = hash(source2);
|
|
1642
|
+
owners[id].bases[file] = hash(source2);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
const oldFiles = /* @__PURE__ */ new Map();
|
|
1646
|
+
if (state)
|
|
1647
|
+
for (const owner of Object.values(state.owners))
|
|
1648
|
+
for (const file of Object.keys(owner.files)) {
|
|
1649
|
+
if (oldFiles.has(file))
|
|
1650
|
+
throw new Fault("OWNERSHIP_COLLISION", "Ambiguous previous file ownership.", 5);
|
|
1651
|
+
oldFiles.set(file, owner);
|
|
1652
|
+
}
|
|
1653
|
+
const effectiveSources = { ...input.sources };
|
|
1654
|
+
for (const [file, next] of Object.entries(desired)) {
|
|
1655
|
+
const local = input.sources[file], old = oldFiles.get(file);
|
|
1656
|
+
let content = next;
|
|
1657
|
+
if (local !== void 0 && !old)
|
|
1658
|
+
throw new Fault("UNMANAGED_COLLISION", "A generated path already contains unmanaged source.", 5);
|
|
1659
|
+
if (local !== void 0 && old) {
|
|
1660
|
+
const base2 = input.bases[old.bases[file]];
|
|
1661
|
+
if (base2 === void 0)
|
|
1662
|
+
throw new Fault("GENERATION_BASE_MISSING", "Generated base is unavailable.", 5);
|
|
1663
|
+
content = threeWay(base2, local, next);
|
|
1664
|
+
}
|
|
1665
|
+
effectiveSources[file] = content;
|
|
1666
|
+
const owner = Object.values(owners).find((o) => file in o.files);
|
|
1667
|
+
owner.files[file] = hash(content);
|
|
1668
|
+
if (local !== content)
|
|
1669
|
+
operations.push({
|
|
1670
|
+
path: input.sourceDir + "/" + file,
|
|
1671
|
+
before: input.sourceInventory[file] ?? null,
|
|
1672
|
+
after: hash(content),
|
|
1673
|
+
content,
|
|
1674
|
+
reason: old ? "update-owned-source" : "create-owned-source"
|
|
1675
|
+
});
|
|
1676
|
+
const basePath = `.apexrest-composer/bases/${hash(next)}.apx`;
|
|
1677
|
+
if (!(hash(next) in input.bases))
|
|
1678
|
+
operations.push({
|
|
1679
|
+
path: basePath,
|
|
1680
|
+
before: null,
|
|
1681
|
+
after: hash(next),
|
|
1682
|
+
content: next,
|
|
1683
|
+
reason: "retain-generated-base"
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
const removedFiles = new Set(
|
|
1687
|
+
[...oldFiles].filter(
|
|
1688
|
+
([file, owner]) => !(file in desired) && owners[owner.instanceId]?.mode !== "detached" && owner.mode !== "detached"
|
|
1689
|
+
).map(([file]) => file)
|
|
1690
|
+
);
|
|
1691
|
+
for (const [file, owner] of oldFiles)
|
|
1692
|
+
if (!(file in desired)) {
|
|
1693
|
+
const existing2 = owners[owner.instanceId];
|
|
1694
|
+
if (existing2?.mode === "detached" || owner.mode === "detached") {
|
|
1695
|
+
if (!existing2) owners[owner.instanceId] = owner;
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
if (Object.values(state.owners).some(
|
|
1699
|
+
(o) => o.mode === "detached" && o.allocation.page === owner.allocation.page
|
|
1700
|
+
))
|
|
1701
|
+
throw new Fault("SHARED_CONSUMER_RETAINED", "A detached consumer still uses the owned page.", 5);
|
|
1702
|
+
if (input.sources[file] !== input.bases[owner.bases[file]])
|
|
1703
|
+
throw new Fault(
|
|
1704
|
+
"REMOVAL_CONFLICT",
|
|
1705
|
+
"Owned source has manual changes; detach instead of deleting.",
|
|
1706
|
+
5
|
|
1707
|
+
);
|
|
1708
|
+
const targetPage = declarations(input.sources[file]).find(
|
|
1709
|
+
(n) => n.kind === "page" && n.depth === 0
|
|
1710
|
+
)?.key;
|
|
1711
|
+
if (!targetPage || !/^\d+$/.test(targetPage))
|
|
1712
|
+
throw new Fault("UNKNOWN_CONSUMER_RETAINED", "Removed page identity cannot be verified.", 5);
|
|
1713
|
+
const alias = input.sources[file].match(/^ {4}alias:[ \t]*([A-Za-z0-9_$#]+)[ \t]*$/m)?.[1];
|
|
1714
|
+
for (const [consumer, source2] of Object.entries(effectiveSources)) {
|
|
1715
|
+
if (consumer === file || removedFiles.has(consumer)) continue;
|
|
1716
|
+
const reference = pageReference(source2, targetPage, alias);
|
|
1717
|
+
if (reference)
|
|
1718
|
+
throw new Fault(
|
|
1719
|
+
"UNKNOWN_CONSUMER_RETAINED",
|
|
1720
|
+
reference === "literal" ? "A remaining or unmanaged source still references the removed page." : "A remaining source builds a dynamic page link; review it before removing an owned page.",
|
|
1721
|
+
5
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
operations.push({
|
|
1725
|
+
path: input.sourceDir + "/" + file,
|
|
1726
|
+
before: input.sourceInventory[file],
|
|
1727
|
+
after: null,
|
|
1728
|
+
content: null,
|
|
1729
|
+
reason: "remove-owned-source"
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
const lock = {
|
|
1733
|
+
schemaVersion: 1,
|
|
1734
|
+
generatorVersion: "1",
|
|
1735
|
+
resolverPolicyVersion: "1",
|
|
1736
|
+
blueprintSemanticDigest: blueprintDigest,
|
|
1737
|
+
catalogDigest: catalog3.digest,
|
|
1738
|
+
compatibilityProfile: blueprint.application.compatibilityProfile,
|
|
1739
|
+
packages: Object.fromEntries([...packages].map(([key, p]) => [key, p.digest])),
|
|
1740
|
+
contractDigest: semanticDigest({
|
|
1741
|
+
entities: blueprint.entities,
|
|
1742
|
+
commands: blueprint.commands,
|
|
1743
|
+
contracts: blueprint.contracts
|
|
1744
|
+
})
|
|
1745
|
+
};
|
|
1746
|
+
const lockDigest = semanticDigest(lock), generationDigest = semanticDigest({ blueprintDigest, lockDigest, owners });
|
|
1747
|
+
plan.state = {
|
|
1748
|
+
schemaVersion: 1,
|
|
1749
|
+
generatorVersion: "1",
|
|
1750
|
+
generationDigest,
|
|
1751
|
+
blueprintDigest,
|
|
1752
|
+
lockDigest,
|
|
1753
|
+
owners
|
|
1754
|
+
};
|
|
1755
|
+
plan.lock = lock;
|
|
1756
|
+
plan.status = "materializable";
|
|
1757
|
+
if (input.validation === "source-only")
|
|
1758
|
+
diagnostics.push({
|
|
1759
|
+
code: "SOURCE_ONLY_DRAFT",
|
|
1760
|
+
severity: "warning",
|
|
1761
|
+
message: "Structural draft only; compiler and runtime qualification are unavailable."
|
|
1762
|
+
});
|
|
1763
|
+
diagnostics.push({
|
|
1764
|
+
code: "RUNTIME_NOT_RUN",
|
|
1765
|
+
severity: "info",
|
|
1766
|
+
message: "Composition does not establish live Oracle, import, authorization or browser evidence."
|
|
1767
|
+
});
|
|
1768
|
+
} catch (error) {
|
|
1769
|
+
if (!(error instanceof Fault)) throw error;
|
|
1770
|
+
operations.splice(0);
|
|
1771
|
+
plan.state = null;
|
|
1772
|
+
plan.lock = null;
|
|
1773
|
+
diagnostics.push({ code: error.code, severity: "error", message: error.message });
|
|
1774
|
+
}
|
|
1775
|
+
plan.operations = [...new Map(operations.map((op) => [op.path, op])).values()].sort(
|
|
1776
|
+
(a, b) => a.path < b.path ? -1 : 1
|
|
1777
|
+
);
|
|
1778
|
+
plan.digest = planDigest(plan);
|
|
1779
|
+
const checked = planSchema.safeParse(plan);
|
|
1780
|
+
if (checked.success && Buffer.byteLength(documentText(plan)) <= planLimits.document) return checked.data;
|
|
1781
|
+
plan.status = "blocked";
|
|
1782
|
+
plan.operations = [];
|
|
1783
|
+
plan.state = null;
|
|
1784
|
+
plan.lock = null;
|
|
1785
|
+
plan.diagnostics = [
|
|
1786
|
+
...diagnostics.filter((diagnostic) => diagnostic.severity !== "info"),
|
|
1787
|
+
{
|
|
1788
|
+
code: "PLAN_LIMIT",
|
|
1789
|
+
severity: "error",
|
|
1790
|
+
message: `Generated plan exceeds reviewed limits (owned source up to ${OWNED_TEXT_LIMIT} characters, at most 2048 writes).`
|
|
1791
|
+
}
|
|
1792
|
+
];
|
|
1793
|
+
plan.digest = planDigest(plan);
|
|
1794
|
+
return validate(planSchema, plan);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// packages/core/src/composer/service.ts
|
|
1798
|
+
var composePlanInput = external_exports.strictObject({
|
|
1799
|
+
project: external_exports.string().min(1).max(4096).optional(),
|
|
1800
|
+
blueprint: relativePath.default("app.blueprint.yaml"),
|
|
1801
|
+
out: relativePath,
|
|
1802
|
+
mode: external_exports.enum(["offline", "connected"]).default("offline"),
|
|
1803
|
+
env: refName.optional(),
|
|
1804
|
+
validation: external_exports.enum(["compiler", "source-only"]).default("compiler"),
|
|
1805
|
+
action: external_exports.enum(["compose", "recover-resume", "recover-restore"]).default("compose")
|
|
1806
|
+
});
|
|
1807
|
+
var composeMaterializeInput = external_exports.strictObject({
|
|
1808
|
+
project: external_exports.string().min(1).max(4096).optional(),
|
|
1809
|
+
plan: relativePath.optional(),
|
|
1810
|
+
artifactId: external_exports.uuid().optional(),
|
|
1811
|
+
expectedDigest: digest
|
|
1812
|
+
});
|
|
1813
|
+
async function context(ctx, blueprintFile, envName, oracle) {
|
|
1814
|
+
const env2 = environment(ctx, envName), connection = await resolveConnection(env2.readConnectionRef);
|
|
1815
|
+
const blueprint = await readDocument(ctx.root, blueprintFile, blueprintSchema);
|
|
1816
|
+
const result = { schema: env2.parsingSchema, objects: {}, signatures: {} };
|
|
1817
|
+
async function read(kind, name) {
|
|
1818
|
+
const rows = [];
|
|
1819
|
+
let offset = 0;
|
|
1820
|
+
while (rows.length < 1e4) {
|
|
1821
|
+
const response = await metadataRead(oracle, env2, connection, {
|
|
1822
|
+
kind,
|
|
1823
|
+
name,
|
|
1824
|
+
schema: env2.parsingSchema,
|
|
1825
|
+
offset,
|
|
1826
|
+
limit: 100
|
|
1827
|
+
});
|
|
1828
|
+
if (!("rows" in response))
|
|
1829
|
+
throw new Fault("METADATA_INVALID", "Expected one scoped metadata result.", 5);
|
|
1830
|
+
rows.push(...response.rows);
|
|
1831
|
+
if (response.nextOffset === null) return rows;
|
|
1832
|
+
offset = response.nextOffset;
|
|
1833
|
+
}
|
|
1834
|
+
throw new Fault("METADATA_LIMIT", "Object metadata exceeds the composition limit.", 5);
|
|
1835
|
+
}
|
|
1836
|
+
for (const entity of Object.values(blueprint.entities))
|
|
1837
|
+
if (!result.objects[entity.read.object])
|
|
1838
|
+
result.objects[entity.read.object] = {
|
|
1839
|
+
columns: await read("columns", entity.read.object),
|
|
1840
|
+
constraints: await read("constraints", entity.read.object),
|
|
1841
|
+
constraintColumns: await read("constraint-columns", entity.read.object)
|
|
1842
|
+
};
|
|
1843
|
+
for (const command of Object.values(blueprint.commands))
|
|
1844
|
+
if (!result.signatures[command.package])
|
|
1845
|
+
result.signatures[command.package] = await read("signatures", command.package);
|
|
1846
|
+
return result;
|
|
1847
|
+
}
|
|
1848
|
+
async function composePlan(ctx, request, oracle = new OracleAdapter(), signal) {
|
|
1849
|
+
await requireTrust(ctx.root);
|
|
1850
|
+
if (request.validation === "source-only" && !ctx.config.composer?.allowSourceOnly)
|
|
1851
|
+
throw new Fault(
|
|
1852
|
+
"SOURCE_ONLY_POLICY_REQUIRED",
|
|
1853
|
+
"Source-only composition requires explicit project composer.allowSourceOnly policy.",
|
|
1854
|
+
4
|
|
1855
|
+
);
|
|
1856
|
+
if (request.mode === "connected" && !request.env)
|
|
1857
|
+
throw new Fault("ENVIRONMENT_REQUIRED", "Connected planning requires --env.", 2);
|
|
1858
|
+
if (request.action !== "compose" && request.mode !== "offline")
|
|
1859
|
+
throw new Fault("INVALID_INPUT", "Local recovery has no connected mode.", 2);
|
|
1860
|
+
const pending = await journal(ctx);
|
|
1861
|
+
if (request.action === "compose" && pending && pending.phase !== "completed")
|
|
1862
|
+
throw new Fault(
|
|
1863
|
+
"RECOVERY_REQUIRED",
|
|
1864
|
+
"Create an explicit recovery plan for the interrupted composition.",
|
|
1865
|
+
5
|
|
1866
|
+
);
|
|
1867
|
+
const metadata = request.mode === "connected" ? await context(ctx, request.blueprint, request.env, oracle) : void 0;
|
|
1868
|
+
const input = request.action === "compose" ? await snapshot(ctx, request.blueprint, {
|
|
1869
|
+
mode: request.mode,
|
|
1870
|
+
validation: request.validation,
|
|
1871
|
+
...request.env ? { environment: request.env } : {},
|
|
1872
|
+
...metadata ? { metadata } : {}
|
|
1873
|
+
}) : null;
|
|
1874
|
+
const plan = input ? planComposition(input) : await recoveryPlan(ctx, request.action === "recover-resume" ? "resume" : "restore");
|
|
1875
|
+
let compiler = {
|
|
1876
|
+
status: "not-run",
|
|
1877
|
+
reason: request.action === "compose" ? "Source-only or blocked draft." : "Local journal recovery."
|
|
1878
|
+
};
|
|
1879
|
+
let compilerValidated = false;
|
|
1880
|
+
if (plan.status === "materializable" && request.action === "compose" && request.validation === "compiler") {
|
|
1881
|
+
const staged = await stagePlan(ctx, plan);
|
|
1882
|
+
compiler = await oracle.validate(staged.directory, signal);
|
|
1883
|
+
const actual = compiler;
|
|
1884
|
+
if (actual.mmd.mmdVersion !== "26.1.0+3102" || !/Release 26[.]1[.]/.test(actual.compiler.version))
|
|
1885
|
+
throw new Fault(
|
|
1886
|
+
"PROFILE_COMPILER_MISMATCH",
|
|
1887
|
+
"Real compiler/MMD differs from the pinned Composer profile.",
|
|
1888
|
+
5
|
|
1889
|
+
);
|
|
1890
|
+
compilerValidated = true;
|
|
1891
|
+
}
|
|
1892
|
+
if (metadata)
|
|
1893
|
+
await writeJson(
|
|
1894
|
+
await safePath(ctx.root, `.apexrest/composer/contexts/${plan.contextDigest}.json`),
|
|
1895
|
+
metadata
|
|
1896
|
+
);
|
|
1897
|
+
await freeze(ctx, plan, request.out);
|
|
1898
|
+
const artifactId = await new ArtifactService(ctx).saveJson(plan, "composition-plan");
|
|
1899
|
+
await writeJson(await safePath(ctx.root, `.apexrest/composer/plan-artifacts/${artifactId}.json`), {
|
|
1900
|
+
digest: plan.digest,
|
|
1901
|
+
file: `.apexrest/composer/plans/${plan.digest}.json`
|
|
1902
|
+
});
|
|
1903
|
+
return {
|
|
1904
|
+
status: plan.status,
|
|
1905
|
+
kind: plan.kind,
|
|
1906
|
+
plan: request.out,
|
|
1907
|
+
planDigest: plan.digest,
|
|
1908
|
+
artifactId,
|
|
1909
|
+
diagnostics: plan.diagnostics,
|
|
1910
|
+
operations: plan.operations.map(({ path: path7, reason, before, after }) => ({ path: path7, reason, before, after })),
|
|
1911
|
+
allocationCount: Object.keys(plan.allocations).length,
|
|
1912
|
+
compiler,
|
|
1913
|
+
databaseEffects: [],
|
|
1914
|
+
qualification: compilerValidated ? "offline-compiler" : "unverified",
|
|
1915
|
+
nextActions: plan.status === "materializable" ? ["Review the plan, then materialize using its exact digest."] : ["Resolve the reported binding or ownership diagnostics."]
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
async function composeMaterialize(ctx, request, signal) {
|
|
1919
|
+
if (Boolean(request.plan) === Boolean(request.artifactId))
|
|
1920
|
+
throw new Fault("INVALID_INPUT", "Supply exactly one plan path or registered artifact ID.", 2);
|
|
1921
|
+
let file = request.plan;
|
|
1922
|
+
if (request.artifactId) {
|
|
1923
|
+
const ref = JSON.parse(
|
|
1924
|
+
await readFile2(
|
|
1925
|
+
await safePath(ctx.root, `.apexrest/composer/plan-artifacts/${request.artifactId}.json`),
|
|
1926
|
+
"utf8"
|
|
1927
|
+
)
|
|
1928
|
+
);
|
|
1929
|
+
if (ref.digest !== request.expectedDigest)
|
|
1930
|
+
throw new Fault("PLAN_TAMPERED", "Artifact and expected plan digest differ.", 5);
|
|
1931
|
+
file = ref.file;
|
|
1932
|
+
}
|
|
1933
|
+
return materialize(ctx, await readPlan(ctx, file, request.expectedDigest), signal ? { signal } : {});
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
// packages/core/src/operations.ts
|
|
1937
|
+
var project = external_exports.string().min(1).max(4096).optional();
|
|
1938
|
+
var env = refName;
|
|
1939
|
+
var base = { project };
|
|
1940
|
+
var dependencies = {
|
|
1941
|
+
home: external_exports.string().optional(),
|
|
1942
|
+
yes: external_exports.boolean().default(false),
|
|
1943
|
+
nonInteractive: external_exports.boolean().default(false),
|
|
1944
|
+
offline: external_exports.boolean().default(false),
|
|
1945
|
+
cacheDir: external_exports.string().optional(),
|
|
1946
|
+
dryRun: external_exports.boolean().default(false),
|
|
1947
|
+
acceptOracleLicense: external_exports.boolean().default(false),
|
|
1948
|
+
skipBrowser: external_exports.boolean().default(false),
|
|
1949
|
+
installOsDeps: external_exports.boolean().default(false)
|
|
1950
|
+
};
|
|
1951
|
+
var setup = {
|
|
1952
|
+
...base,
|
|
1953
|
+
...dependencies,
|
|
1954
|
+
from: external_exports.string().optional(),
|
|
1955
|
+
codexHome: external_exports.string().optional(),
|
|
1956
|
+
codex: external_exports.string().min(1).optional(),
|
|
1957
|
+
scope: external_exports.enum(["user", "project"]).default("user"),
|
|
1958
|
+
version: external_exports.string().optional(),
|
|
1959
|
+
nativeOnly: external_exports.boolean().default(false)
|
|
1960
|
+
};
|
|
1961
|
+
var schemas = {
|
|
1962
|
+
version: external_exports.strictObject({}),
|
|
1963
|
+
doctor: external_exports.strictObject(base),
|
|
1964
|
+
"sqlcl.status": external_exports.strictObject({}),
|
|
1965
|
+
"sqlcl.configure": external_exports.strictObject({
|
|
1966
|
+
mode: sqlclMode,
|
|
1967
|
+
mcpRestrictLevel: sqlclRestriction.optional(),
|
|
1968
|
+
databaseTransport: databaseTransport.optional()
|
|
1969
|
+
}),
|
|
1970
|
+
"panel.status": external_exports.strictObject(base),
|
|
1971
|
+
setup: external_exports.strictObject(setup),
|
|
1972
|
+
"dependencies.install": external_exports.strictObject(dependencies),
|
|
1973
|
+
"dependencies.uninstall": external_exports.strictObject({
|
|
1974
|
+
home: external_exports.string().optional(),
|
|
1975
|
+
dryRun: external_exports.boolean().default(false),
|
|
1976
|
+
yes: external_exports.boolean().default(false)
|
|
1977
|
+
}),
|
|
1978
|
+
"plugin.validate": external_exports.strictObject({ ...base, from: external_exports.string().optional() }),
|
|
1979
|
+
"plugin.install": external_exports.strictObject(setup),
|
|
1980
|
+
"plugin.update": external_exports.strictObject({ ...setup, version: external_exports.string().min(1) }),
|
|
1981
|
+
"plugin.uninstall": external_exports.strictObject({
|
|
1982
|
+
...base,
|
|
1983
|
+
home: external_exports.string().optional(),
|
|
1984
|
+
codex: external_exports.string().min(1).optional(),
|
|
1985
|
+
keepRuntime: external_exports.boolean().default(false)
|
|
1986
|
+
}),
|
|
1987
|
+
"project.init": external_exports.strictObject({
|
|
1988
|
+
...base,
|
|
1989
|
+
directory: external_exports.string().min(1),
|
|
1990
|
+
template: external_exports.enum(["blank-app", "customer-crm", "existing-app"]).default("blank-app"),
|
|
1991
|
+
alias: refName.optional()
|
|
1992
|
+
}),
|
|
1993
|
+
"project.adopt": external_exports.strictObject({
|
|
1994
|
+
...base,
|
|
1995
|
+
env,
|
|
1996
|
+
appId: external_exports.number().int().positive(),
|
|
1997
|
+
workingCopy: external_exports.boolean().default(false)
|
|
1998
|
+
}),
|
|
1999
|
+
"project.inspect": external_exports.strictObject({ ...base, detail: external_exports.enum(["full", "summary"]).default("full") }),
|
|
2000
|
+
"connection.add": external_exports.strictObject({
|
|
2001
|
+
...base,
|
|
2002
|
+
name: refName,
|
|
2003
|
+
sqlclName: savedConnectionName.optional(),
|
|
2004
|
+
ordsUrl: ordsUrl.optional(),
|
|
2005
|
+
ordsUsername: ordsUsername.optional(),
|
|
2006
|
+
passwordFile: external_exports.string().min(1).max(4096).optional()
|
|
2007
|
+
}).refine(
|
|
2008
|
+
(value) => !!value.sqlclName || !!(value.ordsUrl && value.ordsUsername),
|
|
2009
|
+
"Supply a direct SQLcl name or ORDS URL and username."
|
|
2010
|
+
),
|
|
2011
|
+
"connection.list": external_exports.strictObject({ ...base, saved: external_exports.boolean().default(false) }),
|
|
2012
|
+
"connection.test": external_exports.strictObject({
|
|
2013
|
+
...base,
|
|
2014
|
+
name: savedConnectionName,
|
|
2015
|
+
saved: external_exports.boolean().default(false)
|
|
2016
|
+
}),
|
|
2017
|
+
"connection.remove": external_exports.strictObject({ ...base, name: refName }),
|
|
2018
|
+
"compose.plan": composePlanInput,
|
|
2019
|
+
"compose.materialize": composeMaterializeInput,
|
|
2020
|
+
"docs.search": external_exports.strictObject({
|
|
2021
|
+
...base,
|
|
2022
|
+
query: external_exports.string().min(1).max(256),
|
|
2023
|
+
corpus: external_exports.enum(["apexlang", "components", "patterns", "blocks", "blueprints"]).default("apexlang"),
|
|
2024
|
+
version: external_exports.string().optional(),
|
|
2025
|
+
kind: external_exports.enum(["grammar", "template", "contract", "guide"]).optional(),
|
|
2026
|
+
family: external_exports.string().min(1).max(200).optional(),
|
|
2027
|
+
profile: external_exports.string().max(200).optional(),
|
|
2028
|
+
status: external_exports.enum(["draft", "experimental", "verified", "deprecated", "revoked"]).optional(),
|
|
2029
|
+
locale: external_exports.enum(["en", "uk"]).optional(),
|
|
2030
|
+
include: external_exports.enum(["code", "metadata"]).optional().describe("search: code on all hits or none"),
|
|
2031
|
+
includeUnresolved: external_exports.boolean().default(false),
|
|
2032
|
+
cursor: external_exports.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
2033
|
+
offset: external_exports.number().int().min(0).max(1e4).default(0),
|
|
2034
|
+
limit: external_exports.number().int().min(1).max(8).default(3)
|
|
2035
|
+
}),
|
|
2036
|
+
"docs.read": external_exports.strictObject({
|
|
2037
|
+
...base,
|
|
2038
|
+
id: external_exports.string().max(200),
|
|
2039
|
+
offset: external_exports.number().int().min(0).default(0),
|
|
2040
|
+
limit: external_exports.number().int().min(1).max(8192).default(4096)
|
|
2041
|
+
}),
|
|
2042
|
+
"docs.sync": external_exports.strictObject({ version: external_exports.string().min(1), dryRun: external_exports.boolean().default(false) }),
|
|
2043
|
+
"metadata.read": metadataInputSchema.extend({ ...base, env }).strict(),
|
|
2044
|
+
"apex.generate": external_exports.strictObject({
|
|
2045
|
+
...base,
|
|
2046
|
+
name: external_exports.string().min(1).max(120),
|
|
2047
|
+
output: relativePath,
|
|
2048
|
+
alias: refName.optional()
|
|
2049
|
+
}),
|
|
2050
|
+
"apex.sync": external_exports.strictObject({ ...base, env, action: external_exports.enum(["init", "status", "refresh", "invalidate"]) }),
|
|
2051
|
+
"apex.export": external_exports.strictObject({ ...base, env, output: relativePath }),
|
|
2052
|
+
"apex.validate": external_exports.strictObject({ ...base, env: env.optional() }),
|
|
2053
|
+
"apex.diff": external_exports.strictObject({ ...base, env, comparison: external_exports.enum(["auto", "live"]).default("auto") }),
|
|
2054
|
+
"db.plan": external_exports.strictObject({ ...base, env }),
|
|
2055
|
+
"deploy.plan": external_exports.strictObject({ ...base, env, out: relativePath }),
|
|
2056
|
+
"deploy.apply": external_exports.strictObject({ ...base, plan: relativePath }),
|
|
2057
|
+
"deploy.status": external_exports.strictObject({ ...base, run: external_exports.uuid() }),
|
|
2058
|
+
"deploy.restore-plan": external_exports.strictObject({ ...base, backup: external_exports.uuid(), out: relativePath }),
|
|
2059
|
+
"test.run": external_exports.strictObject({
|
|
2060
|
+
...base,
|
|
2061
|
+
suite: external_exports.enum(["unit", "sql", "api", "e2e", "all"]),
|
|
2062
|
+
env: env.optional(),
|
|
2063
|
+
headed: external_exports.boolean().default(false)
|
|
2064
|
+
}),
|
|
2065
|
+
"test.report": external_exports.strictObject({ ...base, run: external_exports.uuid() }),
|
|
2066
|
+
"test.auth": external_exports.strictObject({ ...base, env }),
|
|
2067
|
+
"browser.open": external_exports.strictObject({ ...base, env, browserMode: external_exports.enum(["codex", "external"]).optional() }),
|
|
2068
|
+
"jobs.status": external_exports.strictObject({
|
|
2069
|
+
...base,
|
|
2070
|
+
id: external_exports.uuid(),
|
|
2071
|
+
waitSeconds: external_exports.number().int().min(0).max(JOB_WAIT_MAX_SECONDS).default(0)
|
|
2072
|
+
}),
|
|
2073
|
+
"jobs.cancel": external_exports.strictObject({ ...base, id: external_exports.uuid() }),
|
|
2074
|
+
"artifacts.read": external_exports.strictObject({
|
|
2075
|
+
...base,
|
|
2076
|
+
id: external_exports.uuid(),
|
|
2077
|
+
offset: external_exports.number().int().min(0).default(0),
|
|
2078
|
+
limit: external_exports.number().int().min(1).max(16384).default(4096)
|
|
2079
|
+
}),
|
|
2080
|
+
"sandbox.up": external_exports.strictObject(base),
|
|
2081
|
+
"sandbox.status": external_exports.strictObject(base),
|
|
2082
|
+
"sandbox.down": external_exports.strictObject(base),
|
|
2083
|
+
// Composite MCP operations. Each routes to the operations above so the CLI
|
|
2084
|
+
// keeps its granular commands while the agent sees one tool per concern.
|
|
2085
|
+
project: external_exports.strictObject({
|
|
2086
|
+
...base,
|
|
2087
|
+
action: external_exports.enum(["init", "adopt", "inspect", "connection_add", "connection_list", "connection_test"]),
|
|
2088
|
+
directory: external_exports.string().min(1).optional().describe("init: new or empty directory for the project"),
|
|
2089
|
+
template: external_exports.enum(["blank-app", "customer-crm", "existing-app"]).optional(),
|
|
2090
|
+
alias: refName.optional(),
|
|
2091
|
+
env: env.optional(),
|
|
2092
|
+
appId: external_exports.number().int().positive().optional(),
|
|
2093
|
+
workingCopy: external_exports.boolean().optional(),
|
|
2094
|
+
detail: external_exports.enum(["full", "summary"]).default("summary"),
|
|
2095
|
+
name: refName.optional().describe("connection reference name"),
|
|
2096
|
+
sqlclName: savedConnectionName.optional(),
|
|
2097
|
+
ordsUrl: ordsUrl.optional(),
|
|
2098
|
+
ordsUsername: ordsUsername.optional(),
|
|
2099
|
+
passwordFile: external_exports.string().min(1).max(4096).optional(),
|
|
2100
|
+
saved: external_exports.boolean().default(false)
|
|
2101
|
+
}),
|
|
2102
|
+
reference: external_exports.strictObject({
|
|
2103
|
+
...base,
|
|
2104
|
+
mode: external_exports.enum(["search", "read"]),
|
|
2105
|
+
query: external_exports.string().min(1).max(256).optional().describe("search: short English/Ukrainian terms"),
|
|
2106
|
+
id: external_exports.string().max(200).optional().describe("read: result ID, grammar:, component:, pattern:, oracle:"),
|
|
2107
|
+
corpus: external_exports.enum(["apexlang", "components", "patterns", "blocks", "blueprints"]).default("apexlang"),
|
|
2108
|
+
version: external_exports.string().optional(),
|
|
2109
|
+
kind: external_exports.enum(["grammar", "template", "contract", "guide"]).optional(),
|
|
2110
|
+
family: external_exports.string().min(1).max(200).optional(),
|
|
2111
|
+
profile: external_exports.string().max(200).optional(),
|
|
2112
|
+
status: external_exports.enum(["draft", "experimental", "verified", "deprecated", "revoked"]).optional(),
|
|
2113
|
+
locale: external_exports.enum(["en", "uk"]).optional(),
|
|
2114
|
+
include: external_exports.enum(["code", "metadata"]).optional().describe("search: code on all hits or none"),
|
|
2115
|
+
includeUnresolved: external_exports.boolean().default(false),
|
|
2116
|
+
cursor: external_exports.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
2117
|
+
offset: external_exports.number().int().min(0).max(1e7).default(0),
|
|
2118
|
+
limit: external_exports.number().int().min(1).max(8192).optional().describe("search: 1-8 (default 3); read: characters (default 4096)")
|
|
2119
|
+
}),
|
|
2120
|
+
ship: external_exports.strictObject({
|
|
2121
|
+
...base,
|
|
2122
|
+
env,
|
|
2123
|
+
mode: external_exports.enum(["plan", "apply"]).default("plan"),
|
|
2124
|
+
userRequest: external_exports.string().min(10).max(2e3).describe(
|
|
2125
|
+
"The user's literal instruction that authorizes this change (recorded with the deploy grant)"
|
|
2126
|
+
)
|
|
2127
|
+
}),
|
|
2128
|
+
// Internal: the detached worker's apply phase for apexrest_ship.
|
|
2129
|
+
"ship.apply": external_exports.strictObject({
|
|
2130
|
+
...base,
|
|
2131
|
+
env,
|
|
2132
|
+
plan: relativePath,
|
|
2133
|
+
userRequest: external_exports.string().min(10).max(2e3)
|
|
2134
|
+
}),
|
|
2135
|
+
job: external_exports.strictObject({
|
|
2136
|
+
...base,
|
|
2137
|
+
action: external_exports.enum(["status", "cancel"]).default("status"),
|
|
2138
|
+
jobId: external_exports.uuid(),
|
|
2139
|
+
waitSeconds: external_exports.number().int().min(0).max(JOB_WAIT_MAX_SECONDS).default(0)
|
|
2140
|
+
}),
|
|
2141
|
+
status: external_exports.strictObject({ ...base, detail: external_exports.enum(["doctor", "project"]).default("project") })
|
|
2142
|
+
};
|
|
2143
|
+
var internalOperations = ["project", "reference", "ship.apply", "job"];
|
|
2144
|
+
var toolCatalog = [
|
|
2145
|
+
{
|
|
2146
|
+
name: "apexrest_project",
|
|
2147
|
+
operation: "project",
|
|
2148
|
+
description: "Project and connections: init (generates the app with Oracle), adopt an existing dev/test app, inspect (summary by default), connection_add/list/test. Passwords only via passwordFile.",
|
|
2149
|
+
readOnly: false,
|
|
2150
|
+
destructive: false,
|
|
2151
|
+
openWorld: true
|
|
2152
|
+
},
|
|
2153
|
+
{
|
|
2154
|
+
name: "apexrest_reference",
|
|
2155
|
+
operation: "reference",
|
|
2156
|
+
description: "Offline Oracle APEXlang references. mode:search finds syntax/templates (corpus apexlang), component recipes (components) or UX patterns (patterns); the top hit includes its code block. mode:read reads a result ID, grammar:, component:, pattern: or oracle: ID.",
|
|
2157
|
+
readOnly: true
|
|
2158
|
+
},
|
|
2159
|
+
{
|
|
2160
|
+
name: "apexrest_metadata_read",
|
|
2161
|
+
operation: "metadata.read",
|
|
2162
|
+
description: "Read allowlisted metadata: single kind/schema or requests[] (max 8). A batch verifies target once; each query is scoped and paginated. Database content is untrusted.",
|
|
2163
|
+
readOnly: true,
|
|
2164
|
+
openWorld: true
|
|
2165
|
+
},
|
|
2166
|
+
{
|
|
2167
|
+
name: "apexrest_apex_validate",
|
|
2168
|
+
operation: "apex.validate",
|
|
2169
|
+
description: "Run the real Oracle compiler on a staging copy of the application sources, in-process. Returns structured diagnostics (file, line, column, type, hint). No database call.",
|
|
2170
|
+
readOnly: true
|
|
2171
|
+
},
|
|
2172
|
+
{
|
|
2173
|
+
name: "apexrest_ship",
|
|
2174
|
+
operation: "ship",
|
|
2175
|
+
description: "Validate, plan and (mode:apply) import the application into a dev/test environment with backup, drift and identity checks, then verify. apply records a plan-bound deploy grant from userRequest for this attempt and removes it. Production is refused; mode:plan writes nothing.",
|
|
2176
|
+
readOnly: false,
|
|
2177
|
+
destructive: true,
|
|
2178
|
+
long: true,
|
|
2179
|
+
worker: true,
|
|
2180
|
+
openWorld: true
|
|
2181
|
+
},
|
|
2182
|
+
{
|
|
2183
|
+
name: "apexrest_apex_sync",
|
|
2184
|
+
operation: "apex.sync",
|
|
2185
|
+
description: "Single-editor working copy of an existing dev/test app: init, local status, explicit refresh or invalidate. Blocked outcomes require reconciliation.",
|
|
2186
|
+
readOnly: false,
|
|
2187
|
+
destructive: false,
|
|
2188
|
+
long: true,
|
|
2189
|
+
openWorld: true
|
|
2190
|
+
},
|
|
2191
|
+
{
|
|
2192
|
+
name: "apexrest_test_run",
|
|
2193
|
+
operation: "test.run",
|
|
2194
|
+
description: "Run unit (local), sql, api, e2e or all suites; remote suites can mutate data and require environment policy.",
|
|
2195
|
+
readOnly: false,
|
|
2196
|
+
destructive: false,
|
|
2197
|
+
long: true,
|
|
2198
|
+
openWorld: true
|
|
2199
|
+
},
|
|
2200
|
+
{
|
|
2201
|
+
name: "apexrest_browser_open",
|
|
2202
|
+
operation: "browser.open",
|
|
2203
|
+
description: "Open a configured APEX environment in the selected verification browser (codex returns a host handoff; external launches the system browser). Opening is not verification.",
|
|
2204
|
+
readOnly: false,
|
|
2205
|
+
destructive: false,
|
|
2206
|
+
openWorld: true
|
|
2207
|
+
},
|
|
2208
|
+
{
|
|
2209
|
+
name: "apexrest_job",
|
|
2210
|
+
operation: "job",
|
|
2211
|
+
description: "status: read a job (waitSeconds up to 120 waits for completion; phase shows progress). cancel: request cancellation; the database outcome may remain unknown. Reuse the jobId; never rerun work to fetch results.",
|
|
2212
|
+
readOnly: false,
|
|
2213
|
+
destructive: true
|
|
2214
|
+
},
|
|
2215
|
+
{
|
|
2216
|
+
name: "apexrest_artifact_read",
|
|
2217
|
+
operation: "artifacts.read",
|
|
2218
|
+
description: "Read registered sanitized text by opaque ID and bounded range.",
|
|
2219
|
+
readOnly: true
|
|
2220
|
+
},
|
|
2221
|
+
{
|
|
2222
|
+
name: "apexrest_status",
|
|
2223
|
+
operation: "status",
|
|
2224
|
+
description: "detail:doctor inspects local tools (SQLcl, Java, Codex) without downloads; detail:project returns the read-only project snapshot (settings, connections, sync, jobs, deployments, grants). No database call.",
|
|
2225
|
+
readOnly: true
|
|
2226
|
+
}
|
|
2227
|
+
];
|
|
2228
|
+
|
|
2229
|
+
// packages/core/src/service.ts
|
|
2230
|
+
import path6 from "node:path";
|
|
2231
|
+
|
|
2232
|
+
// packages/core/src/doctor.ts
|
|
2233
|
+
import path2 from "node:path";
|
|
2234
|
+
async function doctor() {
|
|
2235
|
+
const state = await runtimeState();
|
|
2236
|
+
const java = process.env.APEXREST_JAVA_HOME ? path2.join(process.env.APEXREST_JAVA_HOME, "bin", process.platform === "win32" ? "java.exe" : "java") : state.java ?? "java";
|
|
2237
|
+
const probes = await Promise.all(
|
|
2238
|
+
[
|
|
2239
|
+
["codex", ["--version"]],
|
|
2240
|
+
[process.env.APEXREST_SQLCL ?? state.sqlcl ?? "sql", ["-version"]],
|
|
2241
|
+
[java, ["-version"]]
|
|
2242
|
+
].map(async ([exe, args]) => {
|
|
2243
|
+
try {
|
|
2244
|
+
const r = await runProcess({
|
|
2245
|
+
executable: exe,
|
|
2246
|
+
args,
|
|
2247
|
+
cwd: process.env.TMPDIR ?? process.cwd(),
|
|
2248
|
+
timeoutMs: 1e4,
|
|
2249
|
+
env: {
|
|
2250
|
+
...process.env,
|
|
2251
|
+
JAVA_HOME: path2.isAbsolute(java) ? path2.dirname(path2.dirname(java)) : process.env.JAVA_HOME
|
|
2252
|
+
}
|
|
2253
|
+
});
|
|
2254
|
+
return {
|
|
2255
|
+
command: exe,
|
|
2256
|
+
informational: exe === "codex",
|
|
2257
|
+
state: r.code === 0 ? "detected" : "unavailable",
|
|
2258
|
+
version: (r.stdout + r.stderr).trim().slice(0, 500)
|
|
2259
|
+
};
|
|
2260
|
+
} catch {
|
|
2261
|
+
return { command: exe, state: "missing", informational: exe === "codex" };
|
|
2262
|
+
}
|
|
2263
|
+
})
|
|
2264
|
+
);
|
|
2265
|
+
return {
|
|
2266
|
+
platform: process.platform,
|
|
2267
|
+
architecture: process.arch,
|
|
2268
|
+
runtime: {
|
|
2269
|
+
executable: process.execPath,
|
|
2270
|
+
version: process.version,
|
|
2271
|
+
baseline: process.versions.node.split(".")[0] === "24"
|
|
2272
|
+
},
|
|
2273
|
+
managedComponents: state.components,
|
|
2274
|
+
sqlcl: await sqlclConfig(),
|
|
2275
|
+
probes,
|
|
2276
|
+
database: "not-configured",
|
|
2277
|
+
nativeHost: "requires-host-verification",
|
|
2278
|
+
telemetry: false
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// packages/core/src/references.ts
|
|
2283
|
+
import path4 from "node:path";
|
|
2284
|
+
import { stat as stat2, readFile as readFile4 } from "node:fs/promises";
|
|
2285
|
+
|
|
2286
|
+
// packages/core/src/reference-index.ts
|
|
2287
|
+
var referenceWords = (text) => text.replace(/([a-z\d])([A-Z])/g, "$1 $2").toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
2288
|
+
function buildReferencePostings(entries) {
|
|
2289
|
+
const postings = /* @__PURE__ */ Object.create(null);
|
|
2290
|
+
entries.forEach((entry2, position) => {
|
|
2291
|
+
for (const word of new Set(referenceWords(entry2.id + " " + (entry2.title ?? "") + " " + entry2.text)))
|
|
2292
|
+
(postings[word] ??= []).push(position);
|
|
2293
|
+
});
|
|
2294
|
+
return postings;
|
|
2295
|
+
}
|
|
2296
|
+
var stopwords = /* @__PURE__ */ new Set(["a", "an", "the", "for", "with", "and", "of", "to", "in", "on", "by"]);
|
|
2297
|
+
var cyrillic = /^[Ѐ-ӿ]+$/u;
|
|
2298
|
+
var ukrainianSuffixes = [
|
|
2299
|
+
"\u0430\u043C\u0438",
|
|
2300
|
+
"\u044F\u043C\u0438",
|
|
2301
|
+
"\u043E\u0432\u0456",
|
|
2302
|
+
"\u0435\u0432\u0456",
|
|
2303
|
+
"\u043E\u0433\u043E",
|
|
2304
|
+
"\u043E\u043C\u0443",
|
|
2305
|
+
"\u0438\u043C\u0438",
|
|
2306
|
+
"\u0456\u043C\u0438",
|
|
2307
|
+
"\u0456\u0441\u0442\u044C",
|
|
2308
|
+
"\u044F\u0445",
|
|
2309
|
+
"\u0430\u0445",
|
|
2310
|
+
"\u0456\u0432",
|
|
2311
|
+
"\u0457\u0432",
|
|
2312
|
+
"\u0430\u043C",
|
|
2313
|
+
"\u044F\u043C",
|
|
2314
|
+
"\u043E\u044E",
|
|
2315
|
+
"\u0435\u044E",
|
|
2316
|
+
"\u0454\u044E",
|
|
2317
|
+
"\u043E\u043C",
|
|
2318
|
+
"\u0435\u043C",
|
|
2319
|
+
"\u0438\u0439",
|
|
2320
|
+
"\u0456\u0439",
|
|
2321
|
+
"\u043E\u0457",
|
|
2322
|
+
"\u0438\u0445",
|
|
2323
|
+
"\u0456\u0445",
|
|
2324
|
+
"\u0430",
|
|
2325
|
+
"\u044F",
|
|
2326
|
+
"\u0443",
|
|
2327
|
+
"\u044E",
|
|
2328
|
+
"\u0456",
|
|
2329
|
+
"\u0438",
|
|
2330
|
+
"\u0435",
|
|
2331
|
+
"\u0454",
|
|
2332
|
+
"\u043E",
|
|
2333
|
+
"\u044C",
|
|
2334
|
+
"\u0439"
|
|
2335
|
+
];
|
|
2336
|
+
function referenceStem(word) {
|
|
2337
|
+
if (cyrillic.test(word)) {
|
|
2338
|
+
if (word.length < 4) return word;
|
|
2339
|
+
for (const suffix of ukrainianSuffixes)
|
|
2340
|
+
if (word.endsWith(suffix) && word.length - suffix.length >= 3) return word.slice(0, -suffix.length);
|
|
2341
|
+
return word;
|
|
2342
|
+
}
|
|
2343
|
+
if (word.length < 4 || /\d$/.test(word)) return word;
|
|
2344
|
+
if (word.endsWith("ies")) return word.slice(0, -3) + "y";
|
|
2345
|
+
if (word.endsWith("sses")) return word.slice(0, -2);
|
|
2346
|
+
if (/(?:x|ch|sh|ss)es$/.test(word)) return word.slice(0, -2);
|
|
2347
|
+
if (word.endsWith("s") && !/(?:ss|us|is)$/.test(word)) return word.slice(0, -1);
|
|
2348
|
+
return word;
|
|
2349
|
+
}
|
|
2350
|
+
var referenceTerms = (text) => referenceWords(text).filter((word) => !stopwords.has(word));
|
|
2351
|
+
var referenceStems = (text) => referenceTerms(text).map(referenceStem);
|
|
2352
|
+
var stemmedReference = (text) => " " + referenceStems(text).join(" ") + " ";
|
|
2353
|
+
var aliases = {
|
|
2354
|
+
textarea: [["text", "area"]],
|
|
2355
|
+
textfield: [["text", "field"]],
|
|
2356
|
+
textbox: [["text", "field"]],
|
|
2357
|
+
datepicker: [["date", "picker"]],
|
|
2358
|
+
dropdown: [["select", "list"]],
|
|
2359
|
+
combo: [["combobox"]],
|
|
2360
|
+
toggle: [["switch"]],
|
|
2361
|
+
graph: [["chart"]],
|
|
2362
|
+
lov: [["list", "value"]],
|
|
2363
|
+
ir: [["interactive", "report"]],
|
|
2364
|
+
ig: [["interactive", "grid"]],
|
|
2365
|
+
da: [["dynamic", "action"]],
|
|
2366
|
+
nav: [["navigation"]],
|
|
2367
|
+
auth: [["authentication"], ["authorization"]],
|
|
2368
|
+
javascript: [["java", "script"]],
|
|
2369
|
+
plsql: [["pl", "sql"]],
|
|
2370
|
+
\u043A\u043D\u043E\u043F\u043A: [["button"]],
|
|
2371
|
+
\u0441\u0442\u043E\u0440\u0456\u043D\u043A: [["page"]],
|
|
2372
|
+
\u0444\u043E\u0440\u043C: [["form"]],
|
|
2373
|
+
\u0434\u0456\u0430\u0433\u0440\u0430\u043C: [["chart"]],
|
|
2374
|
+
\u0433\u0440\u0430\u0444\u0456\u043A: [["chart"]],
|
|
2375
|
+
\u0437\u0432\u0456\u0442: [["report"]],
|
|
2376
|
+
\u0456\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D: [["interactive"]],
|
|
2377
|
+
\u043A\u0430\u0440\u0442\u043A: [["card"]],
|
|
2378
|
+
\u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440: [["calendar"]],
|
|
2379
|
+
\u043F\u0435\u0440\u0435\u043C\u0438\u043A\u0430\u0447: [["switch"]],
|
|
2380
|
+
\u043F\u0435\u0440\u0435\u0432\u0456\u0440\u043A: [["validation"]],
|
|
2381
|
+
\u0432\u0430\u043B\u0456\u0434\u0430\u0446\u0456: [["validation"]],
|
|
2382
|
+
\u043F\u0440\u043E\u0446\u0435\u0441: [["process"]],
|
|
2383
|
+
\u043E\u0431\u0447\u0438\u0441\u043B\u0435\u043D\u043D: [["computation"]],
|
|
2384
|
+
\u0434\u0438\u043D\u0430\u043C\u0456\u0447\u043D: [["dynamic"]],
|
|
2385
|
+
\u0434\u0456\u044F: [["action"]],
|
|
2386
|
+
\u0434\u0456\u0457: [["action"]],
|
|
2387
|
+
\u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0456: [["authorization"]],
|
|
2388
|
+
\u0430\u0432\u0442\u0435\u043D\u0442\u0438\u0444\u0456\u043A\u0430\u0446\u0456: [["authentication"]],
|
|
2389
|
+
\u043D\u0430\u0432\u0456\u0433\u0430\u0446\u0456: [["navigation"]],
|
|
2390
|
+
\u043C\u0435\u043D\u044E: [["menu"]],
|
|
2391
|
+
\u0440\u0435\u0433\u0456\u043E\u043D: [["region"]],
|
|
2392
|
+
\u0444\u0430\u0441\u0435\u0442\u043D: [["faceted"]],
|
|
2393
|
+
\u043F\u043E\u0448\u0443\u043A: [["search"]],
|
|
2394
|
+
\u043C\u043E\u0434\u0430\u043B\u044C\u043D: [["modal"]],
|
|
2395
|
+
\u0432\u0456\u043A\u043D: [["dialog"]],
|
|
2396
|
+
\u0434\u0456\u0430\u043B\u043E\u0433: [["dialog"]],
|
|
2397
|
+
\u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D: [["upload"]],
|
|
2398
|
+
\u0444\u0430\u0439\u043B: [["file"]],
|
|
2399
|
+
\u0434\u0430\u0442: [["date"]],
|
|
2400
|
+
\u0442\u0435\u043A\u0441\u0442: [["text"]],
|
|
2401
|
+
\u043F\u043E\u043B: [["field"]],
|
|
2402
|
+
\u043F\u0440\u0438\u0445\u043E\u0432\u0430\u043D: [["hidden"]],
|
|
2403
|
+
\u0435\u043B\u0435\u043C\u0435\u043D\u0442: [["item"]],
|
|
2404
|
+
\u0441\u043F\u0438\u0441\u043E\u043A: [["list"]],
|
|
2405
|
+
\u0441\u043F\u0438\u0441\u043A: [["list"]],
|
|
2406
|
+
\u0437\u043D\u0430\u0447\u0435\u043D\u043D: [["value"]],
|
|
2407
|
+
\u0441\u0442\u0430\u0442\u0438\u0447\u043D: [["static"]],
|
|
2408
|
+
\u0432\u043C\u0456\u0441\u0442: [["content"]],
|
|
2409
|
+
\u0441\u0456\u0442\u043A: [["grid"]],
|
|
2410
|
+
\u0433\u0440\u0456\u0434: [["grid"]],
|
|
2411
|
+
\u0440\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u043D\u043D: [["edit"]],
|
|
2412
|
+
\u0440\u0435\u0434\u0430\u0433\u043E\u0432\u0430\u043D: [["editable"]],
|
|
2413
|
+
\u0432\u0438\u0431\u0456\u0440: [["select"]],
|
|
2414
|
+
\u043F\u0435\u0440\u0435\u0445\u0456\u0434: [["redirect"], ["branch"]],
|
|
2415
|
+
\u0433\u0456\u043B\u043A: [["branch"]],
|
|
2416
|
+
\u043E\u043D\u043E\u0432\u043B\u0435\u043D\u043D: [["refresh"]],
|
|
2417
|
+
\u043F\u043E\u043A\u0430\u0437\u043D\u0438\u043A: [["metric"]],
|
|
2418
|
+
\u043F\u0430\u043D\u0435\u043B: [["dashboard"], ["panel"]],
|
|
2419
|
+
\u0433\u043E\u043B\u043E\u0432\u043D: [["home"]],
|
|
2420
|
+
\u0432\u0445\u0456\u0434: [["login"]],
|
|
2421
|
+
\u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D: [["global"]],
|
|
2422
|
+
\u043F\u0456\u043A\u0442\u043E\u0433\u0440\u0430\u043C: [["icon"]],
|
|
2423
|
+
\u0456\u043A\u043E\u043D\u043A: [["icon"]],
|
|
2424
|
+
\u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A: [["title"]],
|
|
2425
|
+
\u043A\u043E\u043B\u043E\u043D\u043A: [["column"]],
|
|
2426
|
+
\u0441\u0442\u043E\u0432\u043F\u0447\u0438\u043A: [["bar"]],
|
|
2427
|
+
\u043B\u0456\u043D\u0456\u0439\u043D: [["line"]],
|
|
2428
|
+
\u043A\u0440\u0443\u0433\u043E\u0432: [["pie"]],
|
|
2429
|
+
\u043C\u0430\u043F: [["map"]],
|
|
2430
|
+
\u043A\u0430\u0440\u0442: [["map"]],
|
|
2431
|
+
\u0441\u0435\u043A\u0446\u0456: [["section"]],
|
|
2432
|
+
\u0432\u0432\u0435\u0434\u0435\u043D\u043D: [["entry"], ["data", "entry"]]
|
|
2433
|
+
};
|
|
2434
|
+
function referenceQueryTerms(query) {
|
|
2435
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2436
|
+
const terms = [];
|
|
2437
|
+
for (const stem of referenceStems(query)) {
|
|
2438
|
+
if (seen.has(stem)) continue;
|
|
2439
|
+
seen.add(stem);
|
|
2440
|
+
terms.push({ stem, alternatives: [[stem], ...aliases[stem] ?? []] });
|
|
2441
|
+
if (terms.length === 16) break;
|
|
2442
|
+
}
|
|
2443
|
+
return terms;
|
|
2444
|
+
}
|
|
2445
|
+
var queryPhrase = (terms) => " " + terms.map((term) => term.stem).join(" ") + " ";
|
|
2446
|
+
var routingPattern = /(?:^|[._/])_(?:index|common|shared|template_options|configuration-modules|common_variables)\b|(?:^|\/)README$/;
|
|
2447
|
+
var routingReference = (id, title) => routingPattern.test(id) || routingPattern.test(title);
|
|
2448
|
+
var navigationalWords = /* @__PURE__ */ new Set([
|
|
2449
|
+
"index",
|
|
2450
|
+
"common",
|
|
2451
|
+
"readme",
|
|
2452
|
+
"routing",
|
|
2453
|
+
"contract",
|
|
2454
|
+
"contracts",
|
|
2455
|
+
"load",
|
|
2456
|
+
"order"
|
|
2457
|
+
]);
|
|
2458
|
+
var navigationalQuery = (query) => /[:/]/.test(query.trim()) || referenceWords(query).some((word) => navigationalWords.has(word));
|
|
2459
|
+
function primaryCodeBlock(text, limit = 2500) {
|
|
2460
|
+
const lines = text.split("\n");
|
|
2461
|
+
const blocks = [];
|
|
2462
|
+
let open;
|
|
2463
|
+
for (const line of lines) {
|
|
2464
|
+
if (open) {
|
|
2465
|
+
const close = line.match(/^(\s*)(`{3,}|~{3,})\s*$/);
|
|
2466
|
+
if (close && close[1] === open.indent && close[2][0] === open.fence[0] && close[2].length >= open.fence.length) {
|
|
2467
|
+
blocks.push({ language: open.language, lines: open.lines });
|
|
2468
|
+
open = void 0;
|
|
2469
|
+
} else open.lines.push(line.startsWith(open.indent) ? line.slice(open.indent.length) : line);
|
|
2470
|
+
continue;
|
|
2471
|
+
}
|
|
2472
|
+
const start = line.match(/^(\s*)(`{3,}|~{3,})\s*([\w.+-]*)\s*$/);
|
|
2473
|
+
if (start) open = { indent: start[1], fence: start[2], language: start[3].toLowerCase(), lines: [] };
|
|
2474
|
+
}
|
|
2475
|
+
const chosen = blocks.find((block) => block.language === "apexlang") ?? blocks.find((block) => !block.language) ?? blocks.sort((a, b) => b.lines.join("\n").length - a.lines.join("\n").length)[0];
|
|
2476
|
+
if (!chosen) return null;
|
|
2477
|
+
const full = chosen.lines.join("\n");
|
|
2478
|
+
if (!full.trim()) return null;
|
|
2479
|
+
return boundCode({ language: chosen.language, text: full, truncated: false, length: full.length }, limit);
|
|
2480
|
+
}
|
|
2481
|
+
function boundCode(code2, limit) {
|
|
2482
|
+
if (code2.text.length <= limit) return code2;
|
|
2483
|
+
const cut = code2.text.lastIndexOf("\n", limit);
|
|
2484
|
+
return { ...code2, text: code2.text.slice(0, cut > limit / 2 ? cut : limit), truncated: true };
|
|
2485
|
+
}
|
|
2486
|
+
function codeWanted(include, offset, position) {
|
|
2487
|
+
return include === "code" || include !== "metadata" && offset === 0 && position === 0;
|
|
2488
|
+
}
|
|
2489
|
+
var CODE_LIMIT = 2500;
|
|
2490
|
+
var SEARCH_LINKS = 3;
|
|
2491
|
+
function fitResults(results, budget, size) {
|
|
2492
|
+
const halve = (hit, floor) => {
|
|
2493
|
+
const next = Math.floor(hit.text.length / 2);
|
|
2494
|
+
if (hit.text.length <= floor) return false;
|
|
2495
|
+
hit.text = hit.text.slice(0, Math.max(floor, next));
|
|
2496
|
+
return true;
|
|
2497
|
+
};
|
|
2498
|
+
const shrinkCode = (hit, floor) => {
|
|
2499
|
+
if (!hit.code || hit.code.text.length <= floor) return false;
|
|
2500
|
+
hit.code = boundCode(hit.code, Math.max(floor, Math.floor(hit.code.text.length / 2)));
|
|
2501
|
+
return true;
|
|
2502
|
+
};
|
|
2503
|
+
while (size(results) > budget) {
|
|
2504
|
+
if (results.slice(1).some((hit) => halve(hit, 300))) continue;
|
|
2505
|
+
if (results.some((hit) => shrinkCode(hit, 600))) continue;
|
|
2506
|
+
if (results.slice(1).some((hit) => "relatedReferences" in hit && delete hit.relatedReferences)) continue;
|
|
2507
|
+
if (results.slice(1).some((hit) => "requiresReferences" in hit && delete hit.requiresReferences))
|
|
2508
|
+
continue;
|
|
2509
|
+
if (results.some((hit) => halve(hit, 100))) continue;
|
|
2510
|
+
if (results.some((hit) => hit.code && delete hit.code)) continue;
|
|
2511
|
+
break;
|
|
2512
|
+
}
|
|
2513
|
+
return results;
|
|
2514
|
+
}
|
|
2515
|
+
var identifierQuery = (query) => /^[A-Za-z][\w.:/-]*$/.test(query.trim()) && /[a-z][A-Z]|[-_.:/]/.test(query.trim());
|
|
2516
|
+
var canonicalPattern = /[._/](?:standard|basic|minimal|example|default)(?:-[a-z-]+)?$|\/recipes\/basic$/;
|
|
2517
|
+
var canonicalReference = (id) => canonicalPattern.test(id);
|
|
2518
|
+
var lengthPenalty = (length) => Math.min(150, Math.max(0, Math.log2(length / 2e3)) * 25);
|
|
2519
|
+
function scoreReference(input) {
|
|
2520
|
+
const { terms, matched } = input;
|
|
2521
|
+
if (matched * 2 < terms.length && !input.exact) return null;
|
|
2522
|
+
let titleHits = 0;
|
|
2523
|
+
let titleOrder = 0;
|
|
2524
|
+
for (let i = 0; i < terms.length; i++) {
|
|
2525
|
+
const term = terms[i];
|
|
2526
|
+
let hit = false;
|
|
2527
|
+
for (const alternative of term.alternatives) {
|
|
2528
|
+
hit = true;
|
|
2529
|
+
for (const stem of alternative) if (!input.titleStems.has(stem)) hit = false;
|
|
2530
|
+
if (hit) break;
|
|
2531
|
+
}
|
|
2532
|
+
if (hit) {
|
|
2533
|
+
titleHits++;
|
|
2534
|
+
titleOrder += 50 + 10 * (terms.length - i);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
const coverage = matched / terms.length;
|
|
2538
|
+
let score = (input.exact ? 1e4 : 0) + (input.titleText === input.phrase || input.titleText.startsWith(input.phrase) ? input.titleWeight : 0) + (input.titleText.includes(input.phrase) ? 400 : 0) + titleOrder + (titleHits === terms.length ? 200 : 0) + 300 * coverage * coverage + (input.quoted ? 150 : 0) + input.prior + // Routing documents sink below concrete templates unless the query asks for them.
|
|
2539
|
+
(input.routing ? input.navigational ? 150 : -250 : 0) + (input.canonical && !input.navigational ? 60 : 0) - lengthPenalty(input.length) + 1 / (1 + input.length / 1e3);
|
|
2540
|
+
if (input.bodyText && terms.length) {
|
|
2541
|
+
const body = input.bodyText();
|
|
2542
|
+
if (body.includes(input.phrase)) score += 40;
|
|
2543
|
+
for (let i = 1; i < terms.length; i++)
|
|
2544
|
+
if (body.includes(" " + terms[i - 1].stem + " " + terms[i].stem + " ")) score += 60;
|
|
2545
|
+
}
|
|
2546
|
+
return score;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
// packages/core/src/reference-catalog.ts
|
|
2550
|
+
import path3 from "node:path";
|
|
2551
|
+
import { readFile as readFile3, realpath, stat } from "node:fs/promises";
|
|
2552
|
+
var digest2 = external_exports.string().regex(/^[a-f0-9]{64}$/);
|
|
2553
|
+
var ascii = (max) => external_exports.string().min(1).max(max).regex(/^[\x20-\x7e]+$/);
|
|
2554
|
+
var referenceId = ascii(200);
|
|
2555
|
+
var compatibilitySchema = external_exports.object({
|
|
2556
|
+
apexVersion: ascii(64),
|
|
2557
|
+
themeVersion: ascii(64),
|
|
2558
|
+
mmdVersion: ascii(64)
|
|
2559
|
+
});
|
|
2560
|
+
var entrySchema = external_exports.object({
|
|
2561
|
+
id: referenceId,
|
|
2562
|
+
title: external_exports.string().min(1).max(240),
|
|
2563
|
+
kind: external_exports.enum(["contract", "template", "guide"]),
|
|
2564
|
+
family: ascii(200),
|
|
2565
|
+
version: ascii(120),
|
|
2566
|
+
source: ascii(2048),
|
|
2567
|
+
document: ascii(240),
|
|
2568
|
+
sha256: digest2,
|
|
2569
|
+
searchText: external_exports.string().max(2e5),
|
|
2570
|
+
requires: external_exports.array(referenceId),
|
|
2571
|
+
related: external_exports.array(referenceId),
|
|
2572
|
+
readiness: external_exports.enum(["ready", "reference", "unresolved"]),
|
|
2573
|
+
compatibility: compatibilitySchema,
|
|
2574
|
+
length: external_exports.number().int().nonnegative().optional()
|
|
2575
|
+
});
|
|
2576
|
+
var manifestSchema = external_exports.object({
|
|
2577
|
+
schemaVersion: external_exports.literal(1),
|
|
2578
|
+
catalogVersion: ascii(120),
|
|
2579
|
+
source: external_exports.object({ apexVersion: ascii(64), themeVersion: ascii(64), mmdVersion: ascii(64) }).passthrough(),
|
|
2580
|
+
indexSha256: digest2,
|
|
2581
|
+
files: external_exports.record(external_exports.string(), digest2),
|
|
2582
|
+
counts: external_exports.record(external_exports.string(), external_exports.unknown())
|
|
2583
|
+
});
|
|
2584
|
+
function createReferenceCatalog(definition) {
|
|
2585
|
+
function invalid(message) {
|
|
2586
|
+
throw new Fault(`${definition.faultPrefix}_CATALOG_INVALID`, message, 3);
|
|
2587
|
+
}
|
|
2588
|
+
function relativeFile(file) {
|
|
2589
|
+
if (!/^[A-Za-z0-9_./-]+$/.test(file) || path3.isAbsolute(file) || file.split("/").some((part) => !part || part === "." || part === ".."))
|
|
2590
|
+
invalid(`${definition.label} catalog contains an unsafe file path.`);
|
|
2591
|
+
return file;
|
|
2592
|
+
}
|
|
2593
|
+
async function containedFile(root, file) {
|
|
2594
|
+
const resolved = await realpath(path3.join(root, relativeFile(file)));
|
|
2595
|
+
const relative = path3.relative(root, resolved);
|
|
2596
|
+
if (!relative || relative.startsWith(".." + path3.sep) || relative === ".." || path3.isAbsolute(relative))
|
|
2597
|
+
invalid(`${definition.label} catalog file resolves outside its resource directory.`);
|
|
2598
|
+
return resolved;
|
|
2599
|
+
}
|
|
2600
|
+
async function stamp(file) {
|
|
2601
|
+
const info = await stat(file, { bigint: true });
|
|
2602
|
+
return `${info.dev}:${info.ino}:${info.size}:${info.mtimeNs}:${info.ctimeNs}`;
|
|
2603
|
+
}
|
|
2604
|
+
async function loadIndex(root, manifestFile, indexFile) {
|
|
2605
|
+
try {
|
|
2606
|
+
const manifest = manifestSchema.parse(JSON.parse(await readFile3(manifestFile, "utf8")));
|
|
2607
|
+
for (const file of Object.keys(manifest.files)) relativeFile(file);
|
|
2608
|
+
if (manifest.files["index.json"] !== manifest.indexSha256)
|
|
2609
|
+
invalid(`${definition.label} catalog manifest index hashes disagree.`);
|
|
2610
|
+
const raw = await readFile3(indexFile, "utf8");
|
|
2611
|
+
if (hash(raw) !== manifest.indexSha256)
|
|
2612
|
+
invalid(`${definition.label} catalog index checksum does not match its manifest.`);
|
|
2613
|
+
const entries = external_exports.array(entrySchema).parse(JSON.parse(raw));
|
|
2614
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2615
|
+
const searchable = entries.map((entry2) => {
|
|
2616
|
+
if (!entry2.id.startsWith(definition.prefix))
|
|
2617
|
+
invalid(`${definition.label} catalog contains a reference ID with an invalid prefix.`);
|
|
2618
|
+
relativeFile(entry2.document);
|
|
2619
|
+
if (!entry2.document.startsWith(definition.documentDirectory + "/") || !entry2.document.endsWith(".md"))
|
|
2620
|
+
invalid(
|
|
2621
|
+
`${definition.label} documents must be Markdown files inside ${definition.documentDirectory}/.`
|
|
2622
|
+
);
|
|
2623
|
+
if (manifest.files[entry2.document] !== entry2.sha256)
|
|
2624
|
+
invalid(`${definition.label} document checksum is missing or disagrees with its manifest.`);
|
|
2625
|
+
if (byId.has(entry2.id)) invalid(`${definition.label} catalog contains duplicate reference IDs.`);
|
|
2626
|
+
byId.set(entry2.id, entry2);
|
|
2627
|
+
const titleStems = referenceStems(entry2.title);
|
|
2628
|
+
return {
|
|
2629
|
+
entry: entry2,
|
|
2630
|
+
stems: new Set(referenceStems(`${entry2.id} ${entry2.title} ${entry2.searchText}`)),
|
|
2631
|
+
titleText: " " + titleStems.join(" ") + " ",
|
|
2632
|
+
titleStems: new Set(titleStems),
|
|
2633
|
+
bodyText: void 0,
|
|
2634
|
+
code: void 0
|
|
2635
|
+
};
|
|
2636
|
+
});
|
|
2637
|
+
for (const entry2 of entries)
|
|
2638
|
+
for (const id of [...entry2.requires, ...entry2.related])
|
|
2639
|
+
if (id.startsWith(definition.prefix) && !byId.has(id))
|
|
2640
|
+
invalid(
|
|
2641
|
+
`${definition.label} catalog contains an unresolved ${definition.label.toLowerCase()} reference.`
|
|
2642
|
+
);
|
|
2643
|
+
return { root, manifest, byId, searchable };
|
|
2644
|
+
} catch (error) {
|
|
2645
|
+
if (error instanceof Fault) throw error;
|
|
2646
|
+
invalid(`${definition.label} catalog manifest or index cannot be read or has an invalid format.`);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
let cached2;
|
|
2650
|
+
async function catalogIndex() {
|
|
2651
|
+
let root, manifestFile, indexFile, revision;
|
|
2652
|
+
try {
|
|
2653
|
+
root = await realpath(path3.join(resourceRoot(), definition.directory));
|
|
2654
|
+
manifestFile = await containedFile(root, "manifest.json");
|
|
2655
|
+
indexFile = await containedFile(root, "index.json");
|
|
2656
|
+
revision = await stamp(manifestFile) + ":" + await stamp(indexFile);
|
|
2657
|
+
} catch (error) {
|
|
2658
|
+
cached2 = void 0;
|
|
2659
|
+
if (error instanceof Fault) throw error;
|
|
2660
|
+
throw new Fault(
|
|
2661
|
+
`${definition.faultPrefix}_CATALOG_UNAVAILABLE`,
|
|
2662
|
+
`Install a reviewed release containing the ${definition.label.toLowerCase()} catalog.`,
|
|
2663
|
+
3
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2666
|
+
if (cached2?.root === root && cached2.stamp === revision) return cached2.pending;
|
|
2667
|
+
const pending = loadIndex(root, manifestFile, indexFile);
|
|
2668
|
+
cached2 = { root, stamp: revision, pending };
|
|
2669
|
+
try {
|
|
2670
|
+
return await pending;
|
|
2671
|
+
} catch (error) {
|
|
2672
|
+
if (cached2?.pending === pending) cached2 = void 0;
|
|
2673
|
+
throw error;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
const bytes = (value) => Buffer.byteLength(JSON.stringify(sanitized(value)), "utf8");
|
|
2677
|
+
function metadata(entry2) {
|
|
2678
|
+
return {
|
|
2679
|
+
id: entry2.id,
|
|
2680
|
+
title: entry2.title,
|
|
2681
|
+
kind: entry2.kind,
|
|
2682
|
+
family: entry2.family,
|
|
2683
|
+
version: entry2.version,
|
|
2684
|
+
source: entry2.source,
|
|
2685
|
+
readiness: entry2.readiness,
|
|
2686
|
+
compatibility: entry2.compatibility,
|
|
2687
|
+
classification: definition.classification
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
function links(entry2, count, byId, resolved) {
|
|
2691
|
+
const link = (id) => {
|
|
2692
|
+
const target = byId.get(id);
|
|
2693
|
+
return { id, title: target?.title ?? null, kind: target?.kind ?? null };
|
|
2694
|
+
};
|
|
2695
|
+
return {
|
|
2696
|
+
requires: entry2.requires.slice(0, count),
|
|
2697
|
+
requiresReferences: entry2.requires.slice(0, resolved).map(link),
|
|
2698
|
+
requiresCount: entry2.requires.length,
|
|
2699
|
+
requiresOmittedCount: Math.max(0, entry2.requires.length - count),
|
|
2700
|
+
related: entry2.related.slice(0, count),
|
|
2701
|
+
relatedReferences: entry2.related.slice(0, resolved).map(link),
|
|
2702
|
+
relatedCount: entry2.related.length,
|
|
2703
|
+
relatedOmittedCount: Math.max(0, entry2.related.length - count)
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
async function verifiedDocument(root, entry2) {
|
|
2707
|
+
const raw = await readFile3(await containedFile(root, entry2.document), "utf8");
|
|
2708
|
+
if (hash(raw) !== entry2.sha256)
|
|
2709
|
+
invalid(`${definition.label} document checksum does not match the catalog.`);
|
|
2710
|
+
if (entry2.length !== void 0 && entry2.length !== raw.length)
|
|
2711
|
+
invalid(`${definition.label} document length does not match the catalog.`);
|
|
2712
|
+
return raw;
|
|
2713
|
+
}
|
|
2714
|
+
function window(text, start, length) {
|
|
2715
|
+
let end = Math.min(text.length, start + length);
|
|
2716
|
+
if (end > start && /[\uD800-\uDBFF]/.test(text[end - 1]) && /[\uDC00-\uDFFF]/.test(text[end] ?? ""))
|
|
2717
|
+
end += end - start === 1 ? 1 : -1;
|
|
2718
|
+
return text.slice(start, end);
|
|
2719
|
+
}
|
|
2720
|
+
async function search(query, version, options = {}) {
|
|
2721
|
+
const terms = referenceQueryTerms(query);
|
|
2722
|
+
if (!terms.length) return [];
|
|
2723
|
+
const index = await catalogIndex();
|
|
2724
|
+
const phrase = queryPhrase(terms);
|
|
2725
|
+
const navigational = navigationalQuery(query);
|
|
2726
|
+
const exactId = query.trim();
|
|
2727
|
+
const identifier2 = identifierQuery(query);
|
|
2728
|
+
const ranked = index.searchable.filter(
|
|
2729
|
+
({ entry: entry2 }) => entry2.id === exactId || (!version || entry2.version === version || !version.includes("@") && entry2.version.split("@")[0] === version) && (!options.kind || entry2.kind === options.kind) && (!options.family || entry2.family === options.family || entry2.family.startsWith(options.family + "/")) && // Unresolved records have no usable recipe; they stay discoverable on request.
|
|
2730
|
+
(options.includeUnresolved || entry2.readiness !== "unresolved")
|
|
2731
|
+
).map((candidate) => ({
|
|
2732
|
+
entry: candidate.entry,
|
|
2733
|
+
score: scoreReference({
|
|
2734
|
+
terms,
|
|
2735
|
+
phrase,
|
|
2736
|
+
navigational,
|
|
2737
|
+
exact: candidate.entry.id === exactId,
|
|
2738
|
+
matched: terms.filter(
|
|
2739
|
+
(term) => term.alternatives.some((alternative) => alternative.every((stem) => candidate.stems.has(stem)))
|
|
2740
|
+
).length,
|
|
2741
|
+
titleText: candidate.titleText,
|
|
2742
|
+
titleStems: candidate.titleStems,
|
|
2743
|
+
titleWeight: 2e3,
|
|
2744
|
+
bodyText: () => candidate.bodyText ??= stemmedReference(candidate.entry.searchText),
|
|
2745
|
+
// Ready recipes first; parameter contracts for property-name lookups.
|
|
2746
|
+
prior: (candidate.entry.readiness === "ready" ? 10 : 0) + (candidate.entry.kind === "template" ? 10 : 0) + (candidate.entry.kind === "contract" ? 5 + (identifier2 ? 100 : 0) : 0),
|
|
2747
|
+
routing: false,
|
|
2748
|
+
canonical: canonicalReference(candidate.entry.id),
|
|
2749
|
+
length: candidate.entry.searchText.length
|
|
2750
|
+
})
|
|
2751
|
+
})).filter((hit) => hit.score !== null).sort((a, b) => b.score - a.score || (a.entry.id < b.entry.id ? -1 : a.entry.id > b.entry.id ? 1 : 0));
|
|
2752
|
+
const offset = Math.max(0, options.offset ?? 0), limit = Math.max(1, Math.min(8, options.limit ?? 3));
|
|
2753
|
+
const words = referenceTerms(query);
|
|
2754
|
+
const candidates = [];
|
|
2755
|
+
for (const [i, { entry: entry2 }] of ranked.slice(offset, offset + limit).entries()) {
|
|
2756
|
+
const first = words.map((term) => entry2.searchText.toLowerCase().indexOf(term)).filter((n) => n >= 0);
|
|
2757
|
+
const matchOffset = first.length ? Math.min(...first) : null;
|
|
2758
|
+
let snippetOffset = Math.max(0, (matchOffset ?? 0) - 80);
|
|
2759
|
+
if (snippetOffset && /[\uDC00-\uDFFF]/.test(entry2.searchText[snippetOffset] ?? "") && /[\uD800-\uDBFF]/.test(entry2.searchText[snippetOffset - 1] ?? ""))
|
|
2760
|
+
snippetOffset--;
|
|
2761
|
+
const text = window(entry2.searchText, snippetOffset, 600);
|
|
2762
|
+
const code2 = codeWanted(options.include, offset, i) ? await documentCode(index, entry2) : void 0;
|
|
2763
|
+
candidates.push({
|
|
2764
|
+
...metadata(entry2),
|
|
2765
|
+
...links(entry2, 2, index.byId, SEARCH_LINKS),
|
|
2766
|
+
text,
|
|
2767
|
+
...code2 ? { code: code2 } : {},
|
|
2768
|
+
// Index summaries are intentionally independent of documents; read a result at offset 0.
|
|
2769
|
+
snippetSource: "index",
|
|
2770
|
+
readOffset: 0,
|
|
2771
|
+
offset: snippetOffset,
|
|
2772
|
+
matchOffset,
|
|
2773
|
+
length: entry2.searchText.length,
|
|
2774
|
+
nextOffset: null,
|
|
2775
|
+
totalMatches: ranked.length,
|
|
2776
|
+
nextResultOffset: null
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
fitResults(candidates, 7e3, bytes);
|
|
2780
|
+
const results = [];
|
|
2781
|
+
for (const hit of candidates) {
|
|
2782
|
+
while (bytes([...results, hit]) > 7e3 && hit.text.length) {
|
|
2783
|
+
const remaining = Math.floor(hit.text.length / 2);
|
|
2784
|
+
hit.text = remaining < 2 ? "" : window(hit.text, 0, remaining);
|
|
2785
|
+
}
|
|
2786
|
+
if (bytes([...results, hit]) > 7e3) break;
|
|
2787
|
+
results.push(hit);
|
|
2788
|
+
}
|
|
2789
|
+
if (!results.length && candidates.length)
|
|
2790
|
+
invalid(`${definition.label} result metadata exceeds the response budget.`);
|
|
2791
|
+
const next = offset + results.length < ranked.length ? offset + results.length : null;
|
|
2792
|
+
for (const result of results) result.nextResultOffset = next;
|
|
2793
|
+
return results;
|
|
2794
|
+
}
|
|
2795
|
+
async function documentCode(index, entry2) {
|
|
2796
|
+
const candidate = index.searchable.find((item2) => item2.entry === entry2);
|
|
2797
|
+
if (candidate.code === void 0) {
|
|
2798
|
+
try {
|
|
2799
|
+
candidate.code = primaryCodeBlock(redact(await verifiedDocument(index.root, entry2)), CODE_LIMIT);
|
|
2800
|
+
} catch {
|
|
2801
|
+
candidate.code = null;
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
return candidate.code ? boundCode(candidate.code, CODE_LIMIT) : void 0;
|
|
2805
|
+
}
|
|
2806
|
+
async function read(id, offset, limit) {
|
|
2807
|
+
const index = await catalogIndex();
|
|
2808
|
+
const entry2 = index.byId.get(id);
|
|
2809
|
+
if (!entry2)
|
|
2810
|
+
throw new Fault(
|
|
2811
|
+
"REFERENCE_NOT_FOUND",
|
|
2812
|
+
`No registered ${definition.label.toLowerCase()} reference with this ID.`,
|
|
2813
|
+
2
|
|
2814
|
+
);
|
|
2815
|
+
let raw;
|
|
2816
|
+
try {
|
|
2817
|
+
raw = await verifiedDocument(index.root, entry2);
|
|
2818
|
+
} catch (error) {
|
|
2819
|
+
if (error instanceof Fault) throw error;
|
|
2820
|
+
invalid(`${definition.label} document cannot be read.`);
|
|
2821
|
+
}
|
|
2822
|
+
const navigation = "\n\n## Catalog navigation\n\n" + entry2.requires.map((target) => `- requires: ${target}
|
|
2823
|
+
`).join("") + entry2.related.map((target) => `- related: ${target}
|
|
2824
|
+
`).join("");
|
|
2825
|
+
const safeRaw = redact(raw);
|
|
2826
|
+
const document = safeRaw + redact(navigation);
|
|
2827
|
+
const start = Math.max(0, offset);
|
|
2828
|
+
let count = Math.max(1, Math.min(8192, limit));
|
|
2829
|
+
const create = () => artifactPage(document, "text", id, start, count, {
|
|
2830
|
+
...metadata(entry2),
|
|
2831
|
+
...links(entry2, 16, index.byId, 16),
|
|
2832
|
+
length: document.length,
|
|
2833
|
+
documentLength: safeRaw.length,
|
|
2834
|
+
sourceDocumentLength: raw.length,
|
|
2835
|
+
contentSanitized: true,
|
|
2836
|
+
navigationOffset: safeRaw.length,
|
|
2837
|
+
sha256: entry2.sha256
|
|
2838
|
+
});
|
|
2839
|
+
let result = create();
|
|
2840
|
+
while (bytes(result) > 3e4 && count > 1) {
|
|
2841
|
+
count = Math.max(1, Math.floor(count / 2));
|
|
2842
|
+
result = create();
|
|
2843
|
+
}
|
|
2844
|
+
if (bytes(result) > 3e4) invalid(`${definition.label} document metadata exceeds the response budget.`);
|
|
2845
|
+
return result;
|
|
2846
|
+
}
|
|
2847
|
+
return { search, read };
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// packages/core/src/components.ts
|
|
2851
|
+
var catalog = createReferenceCatalog({
|
|
2852
|
+
directory: "components",
|
|
2853
|
+
prefix: "component:",
|
|
2854
|
+
documentDirectory: "documents",
|
|
2855
|
+
label: "Component",
|
|
2856
|
+
faultPrefix: "COMPONENT",
|
|
2857
|
+
classification: "component-reference-data"
|
|
2858
|
+
});
|
|
2859
|
+
var componentSearch = catalog.search;
|
|
2860
|
+
var componentRead = catalog.read;
|
|
2861
|
+
|
|
2862
|
+
// packages/core/src/patterns.ts
|
|
2863
|
+
var catalog2 = createReferenceCatalog({
|
|
2864
|
+
directory: "patterns",
|
|
2865
|
+
prefix: "pattern:",
|
|
2866
|
+
documentDirectory: "docs",
|
|
2867
|
+
label: "Pattern",
|
|
2868
|
+
faultPrefix: "PATTERN",
|
|
2869
|
+
classification: "pattern-reference-data"
|
|
2870
|
+
});
|
|
2871
|
+
var patternSearch = catalog2.search;
|
|
2872
|
+
var patternRead = catalog2.read;
|
|
2873
|
+
|
|
2874
|
+
// packages/core/src/references.ts
|
|
2875
|
+
var references = [
|
|
2876
|
+
{
|
|
2877
|
+
id: "apexlang-lifecycle",
|
|
2878
|
+
version: "26.1",
|
|
2879
|
+
source: "https://docs.oracle.com/en/database/oracle/sql-developer-command-line/26.1/sqcug/apexlang.html",
|
|
2880
|
+
text: 'Generate starter files using apex generate -name "Name" -dir ./fresh. Validate with apex validate -input ./application. Export requires a connection and always uses fresh staging. Import deploys the full application and requires a reviewed plan. Preserve .apex/apexlang.json and its compiler metadata.'
|
|
2881
|
+
},
|
|
2882
|
+
{
|
|
2883
|
+
id: "deployment-safety",
|
|
2884
|
+
version: "1.0.0",
|
|
2885
|
+
source: "docs/adr/007-clean-apex-deployment.md",
|
|
2886
|
+
text: "Use an explicit environment. Plans bind source hashes and target identity. Recheck drift, acquire local coordination and create an export backup before writes. Clean APEX deployment needs no service tables. Local runners must share one managed home; independent machines need external serialization. DDL cannot be generally rolled back. Interrupted writes require reconciliation. Production requires an external approval boundary."
|
|
2887
|
+
}
|
|
2888
|
+
];
|
|
2889
|
+
function versionMatches(actual, requested) {
|
|
2890
|
+
if (!requested) return true;
|
|
2891
|
+
return actual === requested || !requested.includes("@") && actual.split("@")[0] === requested;
|
|
2892
|
+
}
|
|
2893
|
+
function indexReferences(upstream, file, digest3) {
|
|
2894
|
+
const entries = [...references, ...upstream];
|
|
2895
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2896
|
+
const bySymbol = /* @__PURE__ */ new Map();
|
|
2897
|
+
const positionById = /* @__PURE__ */ new Map();
|
|
2898
|
+
const searchable = entries.map((reference, position) => {
|
|
2899
|
+
if (!byId.has(reference.id)) {
|
|
2900
|
+
byId.set(reference.id, reference);
|
|
2901
|
+
positionById.set(reference.id, position);
|
|
2902
|
+
}
|
|
2903
|
+
const symbol = reference.text.match(/^<([^>\n]+)>\s*::=/)?.[1];
|
|
2904
|
+
if (symbol) bySymbol.set(symbol, reference.id);
|
|
2905
|
+
const title = reference.title ?? symbol ?? reference.id;
|
|
2906
|
+
return {
|
|
2907
|
+
reference,
|
|
2908
|
+
title,
|
|
2909
|
+
// Title stems, routing/canonical flags and normalized bodies are derived on first use.
|
|
2910
|
+
titleText: void 0,
|
|
2911
|
+
titleStems: void 0,
|
|
2912
|
+
routing: void 0,
|
|
2913
|
+
canonical: void 0,
|
|
2914
|
+
lower: void 0,
|
|
2915
|
+
bodyText: void 0,
|
|
2916
|
+
code: void 0
|
|
2917
|
+
};
|
|
2918
|
+
});
|
|
2919
|
+
let pendingPostings;
|
|
2920
|
+
const postings = () => pendingPostings ??= (async () => {
|
|
2921
|
+
if (file) {
|
|
2922
|
+
try {
|
|
2923
|
+
const prebuilt = await readJson(path4.join(path4.dirname(file), "search.json"));
|
|
2924
|
+
if (prebuilt && prebuilt.schemaVersion === 1 && prebuilt.indexSha256 === digest3 && prebuilt.postings && Object.values(prebuilt.postings).every(
|
|
2925
|
+
(list) => Array.isArray(list) && list.every(
|
|
2926
|
+
(n) => Number.isInteger(n) && Number(n) >= 0 && Number(n) < upstream.length
|
|
2927
|
+
)
|
|
2928
|
+
)) {
|
|
2929
|
+
const result = buildReferencePostings(references);
|
|
2930
|
+
for (const [term, list] of Object.entries(prebuilt.postings))
|
|
2931
|
+
result[term] = [
|
|
2932
|
+
...result[term] ?? [],
|
|
2933
|
+
...list.map((position) => position + references.length)
|
|
2934
|
+
];
|
|
2935
|
+
return result;
|
|
2936
|
+
}
|
|
2937
|
+
} catch {
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
return buildReferencePostings(entries);
|
|
2941
|
+
})();
|
|
2942
|
+
let stemWords;
|
|
2943
|
+
const stemSets = /* @__PURE__ */ new Map();
|
|
2944
|
+
const positionsForStem = async (stem) => {
|
|
2945
|
+
let set = stemSets.get(stem);
|
|
2946
|
+
if (set) return set;
|
|
2947
|
+
const lists = await postings();
|
|
2948
|
+
if (!stemWords) {
|
|
2949
|
+
stemWords = /* @__PURE__ */ new Map();
|
|
2950
|
+
for (const word of Object.keys(lists)) {
|
|
2951
|
+
const key = referenceStem(word);
|
|
2952
|
+
const group2 = stemWords.get(key);
|
|
2953
|
+
if (group2) group2.push(word);
|
|
2954
|
+
else stemWords.set(key, [word]);
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
set = /* @__PURE__ */ new Set();
|
|
2958
|
+
for (const word of stemWords.get(stem) ?? []) for (const position of lists[word] ?? []) set.add(position);
|
|
2959
|
+
stemSets.set(stem, set);
|
|
2960
|
+
return set;
|
|
2961
|
+
};
|
|
2962
|
+
const link = (id) => {
|
|
2963
|
+
const position = positionById.get(id);
|
|
2964
|
+
const entry2 = position === void 0 ? void 0 : searchable[position];
|
|
2965
|
+
return { id, title: entry2?.title ?? null, kind: entry2?.reference.kind ?? null };
|
|
2966
|
+
};
|
|
2967
|
+
return {
|
|
2968
|
+
upstream,
|
|
2969
|
+
byId,
|
|
2970
|
+
bySymbol,
|
|
2971
|
+
positionById,
|
|
2972
|
+
searchable,
|
|
2973
|
+
postings,
|
|
2974
|
+
positionsForStem,
|
|
2975
|
+
link,
|
|
2976
|
+
queries: /* @__PURE__ */ new Map()
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
var cached;
|
|
2980
|
+
async function referenceIndex() {
|
|
2981
|
+
const file = path4.join(resourceRoot(), "references/index.json");
|
|
2982
|
+
let info;
|
|
2983
|
+
try {
|
|
2984
|
+
info = await stat2(file, { bigint: true });
|
|
2985
|
+
} catch (error) {
|
|
2986
|
+
if (error.code !== "ENOENT") throw error;
|
|
2987
|
+
cached = void 0;
|
|
2988
|
+
return indexReferences([]);
|
|
2989
|
+
}
|
|
2990
|
+
const stamp = `${info.dev}:${info.ino}:${info.size}:${info.mtimeNs}:${info.ctimeNs}`;
|
|
2991
|
+
if (cached?.file === file && cached.stamp === stamp) return cached.pending;
|
|
2992
|
+
const pending = readFile4(file, "utf8").then(
|
|
2993
|
+
(raw) => indexReferences(JSON.parse(raw), file, hash(raw))
|
|
2994
|
+
);
|
|
2995
|
+
cached = { file, stamp, pending };
|
|
2996
|
+
try {
|
|
2997
|
+
return await pending;
|
|
2998
|
+
} catch (error) {
|
|
2999
|
+
if (cached?.pending === pending) cached = void 0;
|
|
3000
|
+
throw error;
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
function snippet(text, lower, query, terms) {
|
|
3004
|
+
let matchOffset = lower.indexOf(query.trim().toLowerCase());
|
|
3005
|
+
if (matchOffset < 0) {
|
|
3006
|
+
const pattern = referenceWords(query).join("[\\s._:-]*");
|
|
3007
|
+
if (pattern) matchOffset = lower.search(new RegExp(pattern, "u"));
|
|
3008
|
+
}
|
|
3009
|
+
if (matchOffset < 0) {
|
|
3010
|
+
const locations = terms.map((term) => lower.indexOf(term)).filter((offset2) => offset2 >= 0);
|
|
3011
|
+
matchOffset = locations.length ? Math.min(...locations) : -1;
|
|
3012
|
+
}
|
|
3013
|
+
const offset = Math.max(0, Math.min(matchOffset - 160, text.length - 1200));
|
|
3014
|
+
return {
|
|
3015
|
+
text: text.slice(offset, offset + 1200),
|
|
3016
|
+
offset,
|
|
3017
|
+
matchOffset: matchOffset < 0 ? null : matchOffset,
|
|
3018
|
+
length: text.length,
|
|
3019
|
+
nextOffset: offset + 1200 < text.length ? offset + 1200 : null
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
async function referenceSearch(query, version, options = {}) {
|
|
3023
|
+
if (options.corpus === "blocks" || options.corpus === "blueprints") {
|
|
3024
|
+
const found = await catalogSearch(query, { ...options, ...version ? { version } : {} });
|
|
3025
|
+
return found.results.map((hit) => ({
|
|
3026
|
+
...hit,
|
|
3027
|
+
source: "bundled-composer-catalog",
|
|
3028
|
+
version: "version" in hit ? hit.version : "1",
|
|
3029
|
+
kind: "template",
|
|
3030
|
+
family: "composer",
|
|
3031
|
+
text: hit.title,
|
|
3032
|
+
offset: 0,
|
|
3033
|
+
matchOffset: 0,
|
|
3034
|
+
length: hit.title.length,
|
|
3035
|
+
nextOffset: null,
|
|
3036
|
+
requires: [],
|
|
3037
|
+
totalMatches: found.totalMatches,
|
|
3038
|
+
nextResultOffset: found.nextResultOffset
|
|
3039
|
+
}));
|
|
3040
|
+
}
|
|
3041
|
+
if (options.corpus === "components") return componentSearch(query, version, options);
|
|
3042
|
+
if (options.corpus === "patterns") return patternSearch(query, version, options);
|
|
3043
|
+
const terms = referenceQueryTerms(query);
|
|
3044
|
+
if (!terms.length) return [];
|
|
3045
|
+
const index = await referenceIndex();
|
|
3046
|
+
const key = JSON.stringify([query.trim(), version, options.kind, options.family]);
|
|
3047
|
+
let ranked = index.queries.get(key);
|
|
3048
|
+
if (!ranked) {
|
|
3049
|
+
const total = index.searchable.length;
|
|
3050
|
+
const counts = new Uint8Array(total);
|
|
3051
|
+
for (const term of terms) {
|
|
3052
|
+
const positions = /* @__PURE__ */ new Set();
|
|
3053
|
+
for (const alternative of term.alternatives) {
|
|
3054
|
+
const sets = [];
|
|
3055
|
+
for (const stem of alternative) sets.push(await index.positionsForStem(stem));
|
|
3056
|
+
sets.sort((a, b) => a.size - b.size);
|
|
3057
|
+
for (const position of sets[0]) if (sets.every((set) => set.has(position))) positions.add(position);
|
|
3058
|
+
}
|
|
3059
|
+
for (const position of positions) counts[position]++;
|
|
3060
|
+
}
|
|
3061
|
+
const phrase = queryPhrase(terms);
|
|
3062
|
+
const navigational = navigationalQuery(query);
|
|
3063
|
+
const identifier2 = identifierQuery(query);
|
|
3064
|
+
const symbolic = identifier2 || options.kind === "grammar" || query.trim().startsWith("grammar:");
|
|
3065
|
+
const exactId = index.byId.get(query.trim()) ?? (symbolic ? index.byId.get(index.bySymbol.get(query.trim().replace(/^grammar:/, "")) ?? "") : void 0);
|
|
3066
|
+
const quotedQuery = '"' + query.trim() + '"';
|
|
3067
|
+
const quotable = identifier2 || options.kind === "grammar";
|
|
3068
|
+
const scoreAt = (position, final2) => {
|
|
3069
|
+
const entry2 = index.searchable[position];
|
|
3070
|
+
const { reference } = entry2;
|
|
3071
|
+
const kind = reference.kind;
|
|
3072
|
+
if (!entry2.titleStems) {
|
|
3073
|
+
const titleStems = referenceStems(entry2.title);
|
|
3074
|
+
entry2.titleText = " " + titleStems.join(" ") + " ";
|
|
3075
|
+
entry2.titleStems = new Set(titleStems);
|
|
3076
|
+
entry2.routing = routingReference(reference.id, entry2.title);
|
|
3077
|
+
entry2.canonical = canonicalReference(reference.id);
|
|
3078
|
+
}
|
|
3079
|
+
return scoreReference({
|
|
3080
|
+
terms,
|
|
3081
|
+
phrase,
|
|
3082
|
+
navigational,
|
|
3083
|
+
exact: reference === exactId,
|
|
3084
|
+
matched: counts[position],
|
|
3085
|
+
titleText: entry2.titleText,
|
|
3086
|
+
titleStems: entry2.titleStems,
|
|
3087
|
+
// A production name wins outright; in prose queries the template of that family does.
|
|
3088
|
+
titleWeight: kind === "grammar" && !identifier2 && options.kind !== "grammar" ? 150 : 2e3,
|
|
3089
|
+
// Prefer concrete templates, then the owning contract; grammar wrappers and incidental
|
|
3090
|
+
// productions rank below unless a production or property name is being looked up.
|
|
3091
|
+
prior: kind === "template" ? 100 : kind === "contract" ? 5 + (identifier2 ? 100 : 0) : kind === "grammar" ? (identifier2 || options.kind === "grammar" ? 0 : -40) + (entry2.title.endsWith("-line") ? -20 : 0) : 0,
|
|
3092
|
+
routing: entry2.routing,
|
|
3093
|
+
canonical: entry2.canonical,
|
|
3094
|
+
length: reference.text.length,
|
|
3095
|
+
// A quoted token marks the production that defines a property or keyword.
|
|
3096
|
+
quoted: quotable && reference.text.includes(quotedQuery),
|
|
3097
|
+
bodyText: final2 ? () => entry2.bodyText ??= stemmedReference(reference.text) : void 0
|
|
3098
|
+
});
|
|
3099
|
+
};
|
|
3100
|
+
const first = [];
|
|
3101
|
+
const half = terms.length / 2;
|
|
3102
|
+
for (let position = 0; position < total; position++) {
|
|
3103
|
+
const r = index.searchable[position].reference;
|
|
3104
|
+
if (counts[position] < half && r !== exactId) continue;
|
|
3105
|
+
if (!versionMatches(r.version, version) || options.kind && r.kind !== options.kind || options.family && r.family !== options.family && !r.family?.startsWith(options.family + "/"))
|
|
3106
|
+
continue;
|
|
3107
|
+
const score = scoreAt(position, false);
|
|
3108
|
+
if (score !== null) first.push({ position, score });
|
|
3109
|
+
}
|
|
3110
|
+
first.sort((a, b) => b.score - a.score || a.position - b.position);
|
|
3111
|
+
let pool = 0;
|
|
3112
|
+
if (terms.length > 1) {
|
|
3113
|
+
let budget = 8e5;
|
|
3114
|
+
for (const { position } of first) {
|
|
3115
|
+
const full = counts[position] === terms.length;
|
|
3116
|
+
budget -= index.searchable[position].reference.text.length;
|
|
3117
|
+
if (pool >= 400 || budget < 0 && pool >= 60 || !full && pool >= 120) break;
|
|
3118
|
+
pool++;
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
const final = first.slice(0, pool).map(({ position }) => ({ position, score: scoreAt(position, true) })).sort((a, b) => b.score - a.score || a.position - b.position);
|
|
3122
|
+
ranked = [...final, ...first.slice(pool)].map(({ position }) => position);
|
|
3123
|
+
if (index.queries.size >= 64) index.queries.delete(index.queries.keys().next().value);
|
|
3124
|
+
index.queries.set(key, ranked);
|
|
3125
|
+
}
|
|
3126
|
+
const offset = options.offset ?? 0;
|
|
3127
|
+
const limit = options.limit ?? 3;
|
|
3128
|
+
const link = index.link;
|
|
3129
|
+
const words = referenceTerms(query);
|
|
3130
|
+
const results = ranked.slice(offset, offset + limit).map((position, i) => {
|
|
3131
|
+
const entry2 = index.searchable[position];
|
|
3132
|
+
const { reference: r, title } = entry2;
|
|
3133
|
+
const requires = r.requires ?? [];
|
|
3134
|
+
const related = r.related ?? [];
|
|
3135
|
+
const code2 = codeWanted(options.include, offset, i) ? entry2.code ??= r.kind === "grammar" ? null : primaryCodeBlock(r.text, CODE_LIMIT) : void 0;
|
|
3136
|
+
return {
|
|
3137
|
+
id: r.id,
|
|
3138
|
+
title,
|
|
3139
|
+
version: r.version,
|
|
3140
|
+
source: r.source,
|
|
3141
|
+
kind: r.kind ?? "guide",
|
|
3142
|
+
family: r.family ?? "workflow",
|
|
3143
|
+
...snippet(r.text, entry2.lower ??= r.text.toLowerCase(), query, words),
|
|
3144
|
+
...code2 ? { code: boundCode(code2, CODE_LIMIT) } : {},
|
|
3145
|
+
requires,
|
|
3146
|
+
requiresReferences: requires.slice(0, SEARCH_LINKS).map(link),
|
|
3147
|
+
requiresCount: requires.length,
|
|
3148
|
+
relatedReferences: related.slice(0, SEARCH_LINKS).map(link),
|
|
3149
|
+
relatedCount: related.length,
|
|
3150
|
+
totalMatches: ranked.length,
|
|
3151
|
+
nextResultOffset: offset + limit < ranked.length ? offset + limit : null
|
|
3152
|
+
};
|
|
3153
|
+
});
|
|
3154
|
+
return fitResults(results, Math.min(16e3, 2500 * limit), (page2) => JSON.stringify(page2).length);
|
|
3155
|
+
}
|
|
3156
|
+
async function referenceRead(id, offset, limit, project2) {
|
|
3157
|
+
if (id.startsWith("block:") || id.startsWith("blueprint:")) return catalogRead(id, offset, limit, project2);
|
|
3158
|
+
if (id.startsWith("component:")) return componentRead(id, offset, limit);
|
|
3159
|
+
if (id.startsWith("pattern:")) return patternRead(id, offset, limit);
|
|
3160
|
+
const index = await referenceIndex();
|
|
3161
|
+
const item2 = index.byId.get(id) ?? index.byId.get(index.bySymbol.get(id.replace(/^grammar:/, "")) ?? "");
|
|
3162
|
+
if (!item2)
|
|
3163
|
+
throw new Fault("REFERENCE_NOT_FOUND", "No registered reference with this ID or grammar symbol.", 2);
|
|
3164
|
+
const content = item2.text.slice(offset, offset + limit);
|
|
3165
|
+
const symbols = [...content.matchAll(/<([^>\n]+)>/g)].map((match2) => index.bySymbol.get(match2[1]));
|
|
3166
|
+
const related = [
|
|
3167
|
+
...new Set(
|
|
3168
|
+
[...item2.related ?? [], ...symbols].filter(
|
|
3169
|
+
(target) => Boolean(target) && target !== item2.id
|
|
3170
|
+
)
|
|
3171
|
+
)
|
|
3172
|
+
];
|
|
3173
|
+
const link = index.link;
|
|
3174
|
+
const requires = item2.requires ?? [];
|
|
3175
|
+
return {
|
|
3176
|
+
id: item2.id,
|
|
3177
|
+
title: index.link(item2.id).title ?? item2.id,
|
|
3178
|
+
version: item2.version,
|
|
3179
|
+
source: item2.source,
|
|
3180
|
+
kind: item2.kind ?? "guide",
|
|
3181
|
+
content,
|
|
3182
|
+
offset,
|
|
3183
|
+
length: item2.text.length,
|
|
3184
|
+
nextOffset: offset + limit < item2.text.length ? offset + limit : null,
|
|
3185
|
+
requires,
|
|
3186
|
+
requiresReferences: requires.slice(0, 16).map(link),
|
|
3187
|
+
related: related.slice(0, 16),
|
|
3188
|
+
// Grammar productions resolve to their names so a relation can be followed without a read.
|
|
3189
|
+
relatedReferences: related.slice(0, 16).map(link),
|
|
3190
|
+
relatedCount: related.length,
|
|
3191
|
+
relatedOmittedCount: Math.max(0, related.length - 16),
|
|
3192
|
+
classification: "vendor-reference-data"
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
async function referenceSync(version, dryRun) {
|
|
3196
|
+
const entries = (await referenceIndex()).upstream.filter((r) => versionMatches(r.version, version));
|
|
3197
|
+
if (!entries.length)
|
|
3198
|
+
throw new Fault(
|
|
3199
|
+
"REFERENCE_VERSION_UNAVAILABLE",
|
|
3200
|
+
"Requested version is not in this reviewed release snapshot. Install a reviewed release containing it.",
|
|
3201
|
+
3
|
|
3202
|
+
);
|
|
3203
|
+
const destination = path4.join(managedHome(), "references", version + ".json");
|
|
3204
|
+
const before = await exists(destination) ? hash(canonical(await readJson(destination))) : null, after = hash(canonical(entries));
|
|
3205
|
+
if (!dryRun && before !== after) await writeJson(destination, entries);
|
|
3206
|
+
return {
|
|
3207
|
+
status: dryRun ? "planned" : before === after ? "unchanged" : "synced",
|
|
3208
|
+
version,
|
|
3209
|
+
before,
|
|
3210
|
+
after,
|
|
3211
|
+
count: entries.length
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
// packages/core/src/sandbox.ts
|
|
3216
|
+
async function sandboxAction(action) {
|
|
3217
|
+
const engines = await Promise.all(
|
|
3218
|
+
["docker", "podman"].map(async (executable) => {
|
|
3219
|
+
try {
|
|
3220
|
+
const r = await runProcess({
|
|
3221
|
+
executable,
|
|
3222
|
+
args: ["info", "--format", "{{json .}}"],
|
|
3223
|
+
cwd: process.cwd(),
|
|
3224
|
+
timeoutMs: 1e4
|
|
3225
|
+
});
|
|
3226
|
+
return { engine: executable, available: r.code === 0 };
|
|
3227
|
+
} catch {
|
|
3228
|
+
return { engine: executable, available: false };
|
|
3229
|
+
}
|
|
3230
|
+
})
|
|
3231
|
+
);
|
|
3232
|
+
const state = {
|
|
3233
|
+
profile: "optional",
|
|
3234
|
+
platform: `${process.platform}/${process.arch}`,
|
|
3235
|
+
engines,
|
|
3236
|
+
supported: false,
|
|
3237
|
+
reason: "No provisioned Oracle DB Free + APEX 26.1 + ORDS artifact tuple has been verified on this host.",
|
|
3238
|
+
volumesRemoved: false
|
|
3239
|
+
};
|
|
3240
|
+
if (action === "status") return state;
|
|
3241
|
+
if (action === "down") return { ...state, status: "not-configured", changed: false };
|
|
3242
|
+
throw new Fault(
|
|
3243
|
+
"SANDBOX_PROFILE_UNVERIFIED",
|
|
3244
|
+
state.reason + " Remote APEX targets remain independent.",
|
|
3245
|
+
3,
|
|
3246
|
+
"blocked"
|
|
3247
|
+
);
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
// packages/core/src/panel.ts
|
|
3251
|
+
import path5 from "node:path";
|
|
3252
|
+
import { readdir as readdir2, realpath as realpath2, stat as stat3 } from "node:fs/promises";
|
|
3253
|
+
var safe = (value) => sanitized(value);
|
|
3254
|
+
var historyLimit = 2e3;
|
|
3255
|
+
var PanelService = class {
|
|
3256
|
+
constructor(root) {
|
|
3257
|
+
this.root = root;
|
|
3258
|
+
}
|
|
3259
|
+
root;
|
|
3260
|
+
async preferences() {
|
|
3261
|
+
return browserPreferences(this.root);
|
|
3262
|
+
}
|
|
3263
|
+
async records(folder) {
|
|
3264
|
+
const base2 = await contained(this.root, ".apexrest/" + folder);
|
|
3265
|
+
if (!await exists(base2)) return { rows: [], omitted: 0 };
|
|
3266
|
+
let entries = (await readdir2(base2, { withFileTypes: true })).filter(
|
|
3267
|
+
(e) => e.isDirectory() && external_exports.uuid().safeParse(e.name).success
|
|
3268
|
+
);
|
|
3269
|
+
let omitted = 0;
|
|
3270
|
+
if (entries.length > historyLimit) {
|
|
3271
|
+
const dated = await Promise.all(
|
|
3272
|
+
entries.map(async (entry2) => ({
|
|
3273
|
+
entry: entry2,
|
|
3274
|
+
at: (await stat3(path5.join(base2, entry2.name)).catch(() => null))?.mtimeMs ?? 0
|
|
3275
|
+
}))
|
|
3276
|
+
);
|
|
3277
|
+
omitted = entries.length - historyLimit;
|
|
3278
|
+
entries = dated.sort((a, b) => b.at - a.at).slice(0, historyLimit).map((d) => d.entry);
|
|
3279
|
+
}
|
|
3280
|
+
const files = await Promise.all(
|
|
3281
|
+
entries.map(async (entry2) => {
|
|
3282
|
+
const file = await contained(base2, entry2.name + "/state.json");
|
|
3283
|
+
const info = await stat3(file).catch(() => null);
|
|
3284
|
+
return { id: entry2.name, file, at: info?.mtimeMs ?? 0, size: info?.size ?? 0 };
|
|
3285
|
+
})
|
|
3286
|
+
);
|
|
3287
|
+
const rows = await Promise.all(
|
|
3288
|
+
files.filter((f) => f.size > 0).sort((a, b) => b.at - a.at).slice(0, 12).map(async (f) => {
|
|
3289
|
+
if (f.size > 2 * 1024 * 1024)
|
|
3290
|
+
return {
|
|
3291
|
+
id: f.id,
|
|
3292
|
+
status: "unavailable",
|
|
3293
|
+
diagnostics: ["Record exceeds the status limit."]
|
|
3294
|
+
};
|
|
3295
|
+
try {
|
|
3296
|
+
return { ...await readJson(f.file), id: f.id };
|
|
3297
|
+
} catch {
|
|
3298
|
+
return {
|
|
3299
|
+
id: f.id,
|
|
3300
|
+
status: "unavailable",
|
|
3301
|
+
diagnostics: ["Cannot read this operation record."]
|
|
3302
|
+
};
|
|
3303
|
+
}
|
|
3304
|
+
})
|
|
3305
|
+
);
|
|
3306
|
+
return { rows, omitted };
|
|
3307
|
+
}
|
|
3308
|
+
async snapshot() {
|
|
3309
|
+
this.root = await realpath2(this.root);
|
|
3310
|
+
const ctx = await loadProject(this.root).catch((error) => {
|
|
3311
|
+
if (error instanceof Fault && error.code === "PROJECT_NOT_CONFIGURED") return null;
|
|
3312
|
+
throw error;
|
|
3313
|
+
});
|
|
3314
|
+
const [prefs, sqlcl, refs, security, jobRecords, deploymentRecords] = await Promise.all([
|
|
3315
|
+
this.preferences(),
|
|
3316
|
+
sqlclConfig(),
|
|
3317
|
+
connections(),
|
|
3318
|
+
policy(),
|
|
3319
|
+
this.records("jobs"),
|
|
3320
|
+
this.records("deployments")
|
|
3321
|
+
]);
|
|
3322
|
+
const jobs = await Promise.all(
|
|
3323
|
+
jobRecords.rows.map(async (row) => {
|
|
3324
|
+
let state = row;
|
|
3325
|
+
if (ctx)
|
|
3326
|
+
try {
|
|
3327
|
+
state = await new JobService(ctx).status(String(row.id));
|
|
3328
|
+
} catch {
|
|
3329
|
+
state = {
|
|
3330
|
+
...row,
|
|
3331
|
+
status: "unavailable",
|
|
3332
|
+
diagnostics: ["Cannot read this job status."]
|
|
3333
|
+
};
|
|
3334
|
+
}
|
|
3335
|
+
const result = state.result ?? {};
|
|
3336
|
+
const diagnostics = Array.isArray(result.diagnostics) ? result.diagnostics : Array.isArray(state.diagnostics) ? state.diagnostics : [];
|
|
3337
|
+
return {
|
|
3338
|
+
id: String(row.id),
|
|
3339
|
+
operation: String(row.operation ?? result.operation ?? "operation"),
|
|
3340
|
+
status: String(result.status ?? state.status),
|
|
3341
|
+
updatedAt: String(state.updatedAt ?? ""),
|
|
3342
|
+
summary: String(result.summary ?? "").slice(0, 1e3),
|
|
3343
|
+
diagnostics: diagnostics.slice(0, 5),
|
|
3344
|
+
artifacts: Array.isArray(result.artifacts) ? result.artifacts.slice(0, 10) : []
|
|
3345
|
+
};
|
|
3346
|
+
})
|
|
3347
|
+
);
|
|
3348
|
+
const deployments = deploymentRecords.rows.map((row) => ({
|
|
3349
|
+
id: String(row.id),
|
|
3350
|
+
status: String(row.state ?? "unknown"),
|
|
3351
|
+
at: String(row.at ?? ""),
|
|
3352
|
+
details: JSON.stringify(safe(row.details ?? {})).slice(0, 1200)
|
|
3353
|
+
}));
|
|
3354
|
+
let changes = { status: "unavailable", files: [] };
|
|
3355
|
+
try {
|
|
3356
|
+
const git = await runProcess({
|
|
3357
|
+
executable: "git",
|
|
3358
|
+
args: [
|
|
3359
|
+
"-c",
|
|
3360
|
+
"core.fsmonitor=false",
|
|
3361
|
+
"-c",
|
|
3362
|
+
"core.untrackedCache=false",
|
|
3363
|
+
"status",
|
|
3364
|
+
"--porcelain=v1",
|
|
3365
|
+
"--untracked-files=normal"
|
|
3366
|
+
],
|
|
3367
|
+
cwd: this.root,
|
|
3368
|
+
timeoutMs: 3e3
|
|
3369
|
+
});
|
|
3370
|
+
if (git.code === 0)
|
|
3371
|
+
changes = { status: "available", files: git.stdout.split("\n").filter(Boolean).slice(0, 80) };
|
|
3372
|
+
} catch {
|
|
3373
|
+
}
|
|
3374
|
+
let toolchain = null;
|
|
3375
|
+
if (ctx) {
|
|
3376
|
+
const file = await contained(this.root, ctx.config.toolchain.lockFile);
|
|
3377
|
+
if (await exists(file)) {
|
|
3378
|
+
if ((await stat3(file)).size <= 128e3) toolchain = { digest: hash(canonical(await readJson(file))) };
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
const sync = ctx ? await Promise.all(
|
|
3382
|
+
Object.entries(ctx.config.environments).map(async ([name, env2]) => {
|
|
3383
|
+
try {
|
|
3384
|
+
return { environment: name, ...await new SyncStore(ctx, env2, name).status() };
|
|
3385
|
+
} catch (error) {
|
|
3386
|
+
return {
|
|
3387
|
+
environment: name,
|
|
3388
|
+
status: "blocked",
|
|
3389
|
+
blocked: true,
|
|
3390
|
+
blockedReason: error instanceof Fault ? error.code : "SYNC_STATE_INVALID",
|
|
3391
|
+
serverFreshness: "not-checked"
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
})
|
|
3395
|
+
) : [];
|
|
3396
|
+
return safe({
|
|
3397
|
+
sync,
|
|
3398
|
+
version: VERSION,
|
|
3399
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3400
|
+
project: this.root,
|
|
3401
|
+
configured: !!ctx,
|
|
3402
|
+
trusted: security.trustedProjects.includes(this.root),
|
|
3403
|
+
configuration: ctx?.config ?? null,
|
|
3404
|
+
sqlcl,
|
|
3405
|
+
preferences: prefs,
|
|
3406
|
+
connections: refs,
|
|
3407
|
+
toolchain,
|
|
3408
|
+
jobs,
|
|
3409
|
+
deployments,
|
|
3410
|
+
history: { jobsOmitted: jobRecords.omitted, deploymentsOmitted: deploymentRecords.omitted },
|
|
3411
|
+
changes,
|
|
3412
|
+
permissions: {
|
|
3413
|
+
activeGrants: security.grants.filter((g) => g.projectRoot === this.root && Date.parse(g.expiresAt) > Date.now()).map((g) => ({ operations: g.operations, expiresAt: g.expiresAt, exactPlan: !!g.planDigest }))
|
|
3414
|
+
}
|
|
3415
|
+
});
|
|
3416
|
+
}
|
|
3417
|
+
};
|
|
3418
|
+
|
|
3419
|
+
// packages/core/src/ship.ts
|
|
3420
|
+
var compilerFaults = /* @__PURE__ */ new Set(["ORACLE_COMMAND_FAILED", "VALIDATION_UNCONFIRMED", "VALIDATION_FAILED"]);
|
|
3421
|
+
function fallbackCompilerDiagnostics(output) {
|
|
3422
|
+
const entries = [];
|
|
3423
|
+
for (const raw of output.split(/\r?\n/)) {
|
|
3424
|
+
const line = raw.replace(/\x1b\[[0-9;]*m/g, "").trim();
|
|
3425
|
+
if (!line || !/\b(?:error|warning|ORA-\d+|PLS-\d+)\b/i.test(line)) continue;
|
|
3426
|
+
if (/^\d+\s+(?:errors?|warnings?)\b|validation (?:failed|completed)/i.test(line)) continue;
|
|
3427
|
+
const entry2 = {
|
|
3428
|
+
severity: /\bwarning\b/i.test(line) && !/\berror\b/i.test(line) ? "warning" : "error",
|
|
3429
|
+
message: line.slice(0, 2e3)
|
|
3430
|
+
};
|
|
3431
|
+
const file = /([\w./-]+\.apx)\b/.exec(line)?.[1];
|
|
3432
|
+
if (file) entry2.file = file;
|
|
3433
|
+
const position = /(?:line\s+(\d+)(?:[,\s]+col(?:umn)?\s+(\d+))?)|\.apx:(\d+)(?::(\d+))?/i.exec(line);
|
|
3434
|
+
if (position) {
|
|
3435
|
+
const l = position[1] ?? position[3], c = position[2] ?? position[4];
|
|
3436
|
+
if (l) entry2.line = Number(l);
|
|
3437
|
+
if (c) entry2.column = Number(c);
|
|
3438
|
+
}
|
|
3439
|
+
entries.push(entry2);
|
|
3440
|
+
}
|
|
3441
|
+
return entries;
|
|
3442
|
+
}
|
|
3443
|
+
function compilerFault(error, parseDiagnostics2) {
|
|
3444
|
+
if (!(error instanceof Fault) || !compilerFaults.has(error.code)) return error;
|
|
3445
|
+
const recorded = Array.isArray(error.details?.diagnostics) ? error.details.diagnostics : [];
|
|
3446
|
+
const diagnostics = recorded.length ? recorded : parseDiagnostics2(error.message);
|
|
3447
|
+
const errors = diagnostics.filter((d) => (d.severity ?? "error") === "error").length;
|
|
3448
|
+
return new Fault(
|
|
3449
|
+
"VALIDATION_FAILED",
|
|
3450
|
+
diagnostics.length ? `Oracle compiler reported ${errors} error(s) and ${diagnostics.length - errors} warning(s).` : error.message.slice(0, 2e3),
|
|
3451
|
+
1,
|
|
3452
|
+
"failed",
|
|
3453
|
+
{
|
|
3454
|
+
...error.details,
|
|
3455
|
+
diagnostics: diagnostics.length ? diagnostics : [{ message: error.message.slice(0, 4e3) }]
|
|
3456
|
+
}
|
|
3457
|
+
);
|
|
3458
|
+
}
|
|
3459
|
+
async function validateApplication(oracle, source2, parseDiagnostics2, signal) {
|
|
3460
|
+
const started = Date.now();
|
|
3461
|
+
let validated;
|
|
3462
|
+
try {
|
|
3463
|
+
validated = await oracle.validate(source2, signal);
|
|
3464
|
+
} catch (error) {
|
|
3465
|
+
throw compilerFault(error, parseDiagnostics2);
|
|
3466
|
+
}
|
|
3467
|
+
const { output, mmd: _mmd, ...rest } = validated;
|
|
3468
|
+
const warnings = parseDiagnostics2(output).filter((d) => d.severity === "warning");
|
|
3469
|
+
return {
|
|
3470
|
+
...rest,
|
|
3471
|
+
diagnostics: warnings.slice(0, 50),
|
|
3472
|
+
warningCount: warnings.length,
|
|
3473
|
+
ms: Date.now() - started,
|
|
3474
|
+
output: output.length > 4e3 ? output.slice(0, 4e3) : output,
|
|
3475
|
+
outputTruncated: output.length > 4e3
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
function sourceCounts(ctx, plan) {
|
|
3479
|
+
const under = (dir) => Object.keys(plan.sources).filter((f) => f.startsWith(dir + "/"));
|
|
3480
|
+
const application = under(ctx.config.application.sourceDir);
|
|
3481
|
+
const pages = application.filter((f) => /\/pages\/[^/]+\.apx$/.test(f));
|
|
3482
|
+
return {
|
|
3483
|
+
application: application.length,
|
|
3484
|
+
pages: pages.length,
|
|
3485
|
+
pageFiles: pages.slice(0, 50).map((f) => f.slice(ctx.config.application.sourceDir.length + 1)),
|
|
3486
|
+
migrations: plan.operations.filter((o) => o.kind === "migration").length,
|
|
3487
|
+
packages: plan.operations.filter((o) => o.kind === "package").length
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3490
|
+
function planPreview(ctx, plan) {
|
|
3491
|
+
return {
|
|
3492
|
+
planId: plan.id,
|
|
3493
|
+
planDigest: plan.digest,
|
|
3494
|
+
environment: plan.environment,
|
|
3495
|
+
targetDigest: plan.targetDigest,
|
|
3496
|
+
planMode: plan.schemaVersion === 1 ? "full-export" : plan.mode,
|
|
3497
|
+
backupRequired: plan.backupRequired,
|
|
3498
|
+
compiler: plan.compiler,
|
|
3499
|
+
createdAt: plan.createdAt,
|
|
3500
|
+
expiresAt: plan.expiresAt,
|
|
3501
|
+
risks: plan.risks,
|
|
3502
|
+
approval: plan.approval,
|
|
3503
|
+
sources: sourceCounts(ctx, plan),
|
|
3504
|
+
target: JSON.stringify(plan.target).length <= 1200 ? plan.target : { omitted: true }
|
|
3505
|
+
};
|
|
3506
|
+
}
|
|
3507
|
+
function applicationLink(ctx, name) {
|
|
3508
|
+
const env2 = environment(ctx, name);
|
|
3509
|
+
return {
|
|
3510
|
+
id: env2.applicationId,
|
|
3511
|
+
alias: ctx.config.application.alias,
|
|
3512
|
+
workspace: env2.workspace,
|
|
3513
|
+
url: new URL(`f?p=${env2.applicationId}`, env2.baseUrl).toString()
|
|
3514
|
+
};
|
|
3515
|
+
}
|
|
3516
|
+
async function shipPlan(ctx, name, deployment, parseDiagnostics2, progress) {
|
|
3517
|
+
await reconcileShipGrants();
|
|
3518
|
+
const started = Date.now();
|
|
3519
|
+
progress?.("validating");
|
|
3520
|
+
let plan;
|
|
3521
|
+
try {
|
|
3522
|
+
plan = await deployment.plan(ctx, name);
|
|
3523
|
+
} catch (error) {
|
|
3524
|
+
throw compilerFault(error, parseDiagnostics2);
|
|
3525
|
+
}
|
|
3526
|
+
const planPath = ".apexrest/plans/ship-" + plan.id + ".json";
|
|
3527
|
+
await writeJson(await contained(ctx.root, planPath), plan);
|
|
3528
|
+
const phases = [{ phase: "planning", ms: Date.now() - started }];
|
|
3529
|
+
return { plan, planPath, phases, preview: planPreview(ctx, plan) };
|
|
3530
|
+
}
|
|
3531
|
+
var phaseFor = {
|
|
3532
|
+
backing_up: "backing_up",
|
|
3533
|
+
migrating: "migrating",
|
|
3534
|
+
importing: "importing",
|
|
3535
|
+
verifying: "verifying",
|
|
3536
|
+
testing: "testing"
|
|
3537
|
+
};
|
|
3538
|
+
function shipGrant(ctx, plan, userRequest) {
|
|
3539
|
+
return {
|
|
3540
|
+
projectRoot: ctx.root,
|
|
3541
|
+
targetDigest: plan.targetDigest,
|
|
3542
|
+
planDigest: plan.digest,
|
|
3543
|
+
expiresAt: new Date(Math.min(Date.parse(plan.expiresAt), Date.now() + 10 * 60 * 1e3)).toISOString(),
|
|
3544
|
+
workerPid: process.pid,
|
|
3545
|
+
operations: ["deploy"],
|
|
3546
|
+
note: userRequest.slice(0, 2e3),
|
|
3547
|
+
grantedBy: "ship",
|
|
3548
|
+
grantedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3549
|
+
};
|
|
3550
|
+
}
|
|
3551
|
+
async function reconcileShipGrants() {
|
|
3552
|
+
await updatePolicy((p) => ({
|
|
3553
|
+
...p,
|
|
3554
|
+
grants: p.grants.filter((g) => {
|
|
3555
|
+
if (g.grantedBy !== "ship") return true;
|
|
3556
|
+
if (Date.parse(g.expiresAt) <= Date.now() || !g.workerPid) return false;
|
|
3557
|
+
try {
|
|
3558
|
+
process.kill(g.workerPid, 0);
|
|
3559
|
+
return true;
|
|
3560
|
+
} catch (error) {
|
|
3561
|
+
return error.code === "EPERM";
|
|
3562
|
+
}
|
|
3563
|
+
})
|
|
3564
|
+
}));
|
|
3565
|
+
}
|
|
3566
|
+
var ownGrant = (ctx, plan) => (g) => g.grantedBy === "ship" && g.projectRoot === ctx.root && g.planDigest === plan.digest;
|
|
3567
|
+
async function checkShipTarget(ctx, plan) {
|
|
3568
|
+
const env2 = environment(ctx, plan.environment);
|
|
3569
|
+
if (await isProductionTarget(env2, plan.targetDigest))
|
|
3570
|
+
throw new Fault(
|
|
3571
|
+
"PRODUCTION_CI_REQUIRED",
|
|
3572
|
+
"Production requires a protected CI runner and externally signed approval bound to this plan.",
|
|
3573
|
+
4,
|
|
3574
|
+
"blocked"
|
|
3575
|
+
);
|
|
3576
|
+
if (plan.risks.some((r) => r !== "application-restore"))
|
|
3577
|
+
throw new Fault(
|
|
3578
|
+
"RECOVERY_REVIEW_REQUIRED",
|
|
3579
|
+
"Destructive, authentication or unsupported changes need an explicit recovery implementation and reviewed external workflow.",
|
|
3580
|
+
4,
|
|
3581
|
+
"blocked",
|
|
3582
|
+
{ nextActions: plan.risks.map((r) => "Review risk: " + r) }
|
|
3583
|
+
);
|
|
3584
|
+
if (ctx.config.tests.requiredSuites.some((suite) => suite !== "unit")) {
|
|
3585
|
+
try {
|
|
3586
|
+
await new TestService().authorize(ctx, plan.environment);
|
|
3587
|
+
} catch (error) {
|
|
3588
|
+
throw new Fault(
|
|
3589
|
+
"TEST_APPROVAL_REQUIRED",
|
|
3590
|
+
"Required remote suites need existing test authorization and a mutation-allowed environment before shipping.",
|
|
3591
|
+
4,
|
|
3592
|
+
"blocked",
|
|
3593
|
+
{ reason: error instanceof Fault ? error.code : "TEST_AUTHORIZATION_FAILED" }
|
|
3594
|
+
);
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
return env2;
|
|
3598
|
+
}
|
|
3599
|
+
async function shipApply(ctx, planValue, userRequest, deployment, lastTests, signal, progress) {
|
|
3600
|
+
const plan = parse(deployPlanSchema, planValue);
|
|
3601
|
+
await checkShipTarget(ctx, plan);
|
|
3602
|
+
await reconcileShipGrants();
|
|
3603
|
+
const phases = [];
|
|
3604
|
+
let current;
|
|
3605
|
+
const mark = (phase) => {
|
|
3606
|
+
if (current) phases.push({ phase: current.phase, ms: Date.now() - current.at });
|
|
3607
|
+
current = { phase, at: Date.now() };
|
|
3608
|
+
progress?.(phase);
|
|
3609
|
+
};
|
|
3610
|
+
const grant = shipGrant(ctx, plan, userRequest);
|
|
3611
|
+
await updatePolicy((p) => ({ ...p, grants: [...p.grants.filter((g) => !ownGrant(ctx, plan)(g)), grant] }));
|
|
3612
|
+
let grantRemoved = false;
|
|
3613
|
+
let applied;
|
|
3614
|
+
try {
|
|
3615
|
+
mark("backing_up");
|
|
3616
|
+
applied = await deployment.apply(ctx, plan, signal, (state) => {
|
|
3617
|
+
const phase = phaseFor[state];
|
|
3618
|
+
if (phase && phase !== current?.phase) mark(phase);
|
|
3619
|
+
});
|
|
3620
|
+
} finally {
|
|
3621
|
+
grantRemoved = await updatePolicy((p) => ({
|
|
3622
|
+
...p,
|
|
3623
|
+
grants: p.grants.filter((g) => !ownGrant(ctx, plan)(g))
|
|
3624
|
+
})).then(
|
|
3625
|
+
() => true,
|
|
3626
|
+
() => false
|
|
3627
|
+
);
|
|
3628
|
+
}
|
|
3629
|
+
if (current) phases.push({ phase: current.phase, ms: Date.now() - current.at });
|
|
3630
|
+
const tests = lastTests();
|
|
3631
|
+
return {
|
|
3632
|
+
status: "succeeded",
|
|
3633
|
+
runId: applied.runId,
|
|
3634
|
+
planId: plan.id,
|
|
3635
|
+
planDigest: plan.digest,
|
|
3636
|
+
environment: plan.environment,
|
|
3637
|
+
application: applicationLink(ctx, plan.environment),
|
|
3638
|
+
sources: sourceCounts(ctx, plan),
|
|
3639
|
+
phases,
|
|
3640
|
+
verification: { identity: "confirmed", state: applied.state, directory: applied.directory },
|
|
3641
|
+
tests: tests ?? (ctx.config.tests.requiredSuites.length ? null : "no-required-suites"),
|
|
3642
|
+
grant: { recorded: true, removed: grantRemoved, expiresAt: grant.expiresAt, planDigest: plan.digest },
|
|
3643
|
+
nextActions: grantRemoved ? ["Verify the affected pages in the selected browser with apexrest_browser_open."] : ["Remove the stale ship grant from APEXREST_HOME/policy.json before the next deployment."]
|
|
3644
|
+
};
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
// packages/core/src/service.ts
|
|
3648
|
+
var engine = oracle_exports;
|
|
3649
|
+
var parseDiagnostics = (output) => (engine.parseCompilerDiagnostics ?? fallbackCompilerDiagnostics)(output);
|
|
3650
|
+
async function sharedOracle() {
|
|
3651
|
+
return new OracleAdapter();
|
|
3652
|
+
}
|
|
3653
|
+
async function shutdownOracle() {
|
|
3654
|
+
const close = engine.closeSqlclSessions ?? (await import("./chunk-BVPAS3TD.mjs").then(
|
|
3655
|
+
(m) => m,
|
|
3656
|
+
() => ({})
|
|
3657
|
+
)).closeSqlclSessions;
|
|
3658
|
+
await close?.().catch(() => void 0);
|
|
3659
|
+
}
|
|
3660
|
+
function routeProject(parsed) {
|
|
3661
|
+
const action = parsed.action;
|
|
3662
|
+
const pick = (...keys) => Object.fromEntries(keys.filter((k) => parsed[k] !== void 0).map((k) => [k, parsed[k]]));
|
|
3663
|
+
switch (action) {
|
|
3664
|
+
case "init":
|
|
3665
|
+
return { operation: "project.init", input: pick("project", "directory", "template", "alias") };
|
|
3666
|
+
case "adopt":
|
|
3667
|
+
return { operation: "project.adopt", input: pick("project", "env", "appId", "workingCopy") };
|
|
3668
|
+
case "inspect":
|
|
3669
|
+
return { operation: "project.inspect", input: pick("project", "detail") };
|
|
3670
|
+
case "connection_add":
|
|
3671
|
+
return {
|
|
3672
|
+
operation: "connection.add",
|
|
3673
|
+
input: pick("project", "name", "sqlclName", "ordsUrl", "ordsUsername", "passwordFile")
|
|
3674
|
+
};
|
|
3675
|
+
case "connection_list":
|
|
3676
|
+
return { operation: "connection.list", input: pick("project", "saved") };
|
|
3677
|
+
default:
|
|
3678
|
+
return { operation: "connection.test", input: pick("project", "name", "saved") };
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
function routeReference(parsed) {
|
|
3682
|
+
const { mode, query, id, limit, offset, ...rest } = parsed;
|
|
3683
|
+
if (mode === "read") {
|
|
3684
|
+
if (typeof id !== "string") throw new Fault("INVALID_INPUT", "id: required for mode read", 2);
|
|
3685
|
+
return {
|
|
3686
|
+
operation: "docs.read",
|
|
3687
|
+
input: { project: rest.project, id, offset, ...limit ? { limit } : {} }
|
|
3688
|
+
};
|
|
3689
|
+
}
|
|
3690
|
+
if (typeof query !== "string") throw new Fault("INVALID_INPUT", "query: required for mode search", 2);
|
|
3691
|
+
if (typeof limit === "number" && limit > 8)
|
|
3692
|
+
throw new Fault("INVALID_INPUT", "limit: search returns at most 8 hits per page", 2);
|
|
3693
|
+
if (typeof offset === "number" && offset > 1e4)
|
|
3694
|
+
throw new Fault("INVALID_INPUT", "offset: search offsets are at most 10000", 2);
|
|
3695
|
+
return { operation: "docs.search", input: { ...rest, query, offset, ...limit ? { limit } : {} } };
|
|
3696
|
+
}
|
|
3697
|
+
async function dispatch(operation, input = {}, signal, progress) {
|
|
3698
|
+
try {
|
|
3699
|
+
if (signal?.aborted)
|
|
3700
|
+
throw new Fault("CANCELLED", "Operation cancelled before execution.", 6, "cancelled");
|
|
3701
|
+
if (!(operation in schemas)) throw new Fault("INVALID_INPUT", `Unknown operation: ${operation}`, 2);
|
|
3702
|
+
const parsed = parse(
|
|
3703
|
+
schemas[operation],
|
|
3704
|
+
input
|
|
3705
|
+
);
|
|
3706
|
+
const text = (key) => parsed[key];
|
|
3707
|
+
const root = text("project") ?? process.cwd();
|
|
3708
|
+
if (operation === "project" || operation === "reference") {
|
|
3709
|
+
const route = operation === "project" ? routeProject(parsed) : routeReference(parsed);
|
|
3710
|
+
return { ...await dispatch(route.operation, route.input, signal, progress), operation };
|
|
3711
|
+
}
|
|
3712
|
+
if (operation === "job") {
|
|
3713
|
+
const result = await dispatch(
|
|
3714
|
+
parsed.action === "cancel" ? "jobs.cancel" : "jobs.status",
|
|
3715
|
+
{
|
|
3716
|
+
project: parsed.project,
|
|
3717
|
+
id: parsed.jobId,
|
|
3718
|
+
...parsed.action === "cancel" ? {} : { waitSeconds: parsed.waitSeconds }
|
|
3719
|
+
},
|
|
3720
|
+
signal
|
|
3721
|
+
);
|
|
3722
|
+
return { ...result, operation };
|
|
3723
|
+
}
|
|
3724
|
+
if (operation === "status") {
|
|
3725
|
+
const result = await dispatch(
|
|
3726
|
+
parsed.detail === "doctor" ? "doctor" : "panel.status",
|
|
3727
|
+
{ project: parsed.project },
|
|
3728
|
+
signal
|
|
3729
|
+
);
|
|
3730
|
+
return { ...result, operation };
|
|
3731
|
+
}
|
|
3732
|
+
const oracle = await sharedOracle(), tests = new TestService(oracle), testReports = [], deployment = new DeploymentService(oracle, async (ctx, env2) => {
|
|
3733
|
+
const report = await tests.all(ctx, env2);
|
|
3734
|
+
testReports.push(report.data);
|
|
3735
|
+
return report;
|
|
3736
|
+
});
|
|
3737
|
+
let data;
|
|
3738
|
+
switch (operation) {
|
|
3739
|
+
case "panel.status":
|
|
3740
|
+
data = await new PanelService(root).snapshot();
|
|
3741
|
+
break;
|
|
3742
|
+
case "version":
|
|
3743
|
+
data = { version: VERSION, node: process.version };
|
|
3744
|
+
break;
|
|
3745
|
+
case "doctor":
|
|
3746
|
+
data = await doctor();
|
|
3747
|
+
break;
|
|
3748
|
+
case "sqlcl.status":
|
|
3749
|
+
data = await sqlclConfig();
|
|
3750
|
+
break;
|
|
3751
|
+
case "sqlcl.configure":
|
|
3752
|
+
data = await configureSqlcl(
|
|
3753
|
+
text("mode"),
|
|
3754
|
+
parsed.mcpRestrictLevel,
|
|
3755
|
+
parsed.databaseTransport
|
|
3756
|
+
);
|
|
3757
|
+
break;
|
|
3758
|
+
case "dependencies.install": {
|
|
3759
|
+
const { ToolchainService } = await import("./chunk-KGTE7Q5Y.mjs");
|
|
3760
|
+
data = await new ToolchainService().apply(parsed);
|
|
3761
|
+
break;
|
|
3762
|
+
}
|
|
3763
|
+
case "dependencies.uninstall": {
|
|
3764
|
+
const { uninstallTools } = await import("./chunk-KKRFM7Q7.mjs");
|
|
3765
|
+
data = await uninstallTools(parsed);
|
|
3766
|
+
break;
|
|
3767
|
+
}
|
|
3768
|
+
case "setup":
|
|
3769
|
+
case "plugin.install":
|
|
3770
|
+
case "plugin.update": {
|
|
3771
|
+
const { setup: setup2 } = await import("./chunk-P3CVQAZW.mjs");
|
|
3772
|
+
data = await setup2(parsed);
|
|
3773
|
+
break;
|
|
3774
|
+
}
|
|
3775
|
+
case "plugin.validate": {
|
|
3776
|
+
const { validateNative } = await import("./chunk-P3CVQAZW.mjs");
|
|
3777
|
+
data = await validateNative(text("from"));
|
|
3778
|
+
break;
|
|
3779
|
+
}
|
|
3780
|
+
case "plugin.uninstall": {
|
|
3781
|
+
const { uninstallNative } = await import("./chunk-P3CVQAZW.mjs");
|
|
3782
|
+
data = await uninstallNative(text("home") ?? managedHome(), Boolean(parsed.keepRuntime), {
|
|
3783
|
+
...text("codex") ? { codex: text("codex") } : {}
|
|
3784
|
+
});
|
|
3785
|
+
break;
|
|
3786
|
+
}
|
|
3787
|
+
case "project.init":
|
|
3788
|
+
data = await projectInit(
|
|
3789
|
+
text("directory"),
|
|
3790
|
+
text("template"),
|
|
3791
|
+
text("alias") ?? path6.basename(path6.resolve(text("directory"))).toLowerCase().replace(/[^a-z0-9-]/g, "-")
|
|
3792
|
+
);
|
|
3793
|
+
break;
|
|
3794
|
+
case "connection.add":
|
|
3795
|
+
data = await configureConnection(text("name"), {
|
|
3796
|
+
sqlclName: text("sqlclName"),
|
|
3797
|
+
ordsUrl: text("ordsUrl"),
|
|
3798
|
+
ordsUsername: text("ordsUsername"),
|
|
3799
|
+
passwordFile: text("passwordFile")
|
|
3800
|
+
});
|
|
3801
|
+
break;
|
|
3802
|
+
case "connection.remove":
|
|
3803
|
+
data = await editConnection(text("name"));
|
|
3804
|
+
break;
|
|
3805
|
+
case "connection.list":
|
|
3806
|
+
data = parsed.saved ? await oracle.savedConnections(signal) : await connections();
|
|
3807
|
+
break;
|
|
3808
|
+
case "connection.test":
|
|
3809
|
+
if (parsed.saved && (await oracle.settings()).databaseTransport === "ords")
|
|
3810
|
+
throw new Fault(
|
|
3811
|
+
"ORDS_SAVED_CONNECTION_UNSUPPORTED",
|
|
3812
|
+
"ORDS uses plugin connection references. Test the configured reference without --saved.",
|
|
3813
|
+
3,
|
|
3814
|
+
"blocked"
|
|
3815
|
+
);
|
|
3816
|
+
data = await oracle.identity(
|
|
3817
|
+
parsed.saved ? { kind: "sqlcl-store", name: text("name") } : await resolveConnection(text("name")),
|
|
3818
|
+
signal
|
|
3819
|
+
);
|
|
3820
|
+
if (parsed.saved) data = { name: text("name"), ...data };
|
|
3821
|
+
break;
|
|
3822
|
+
case "docs.search":
|
|
3823
|
+
data = await referenceSearch(text("query"), text("version"), schemas["docs.search"].parse(parsed));
|
|
3824
|
+
break;
|
|
3825
|
+
case "docs.read":
|
|
3826
|
+
data = await referenceRead(text("id"), Number(parsed.offset), Number(parsed.limit), text("project"));
|
|
3827
|
+
break;
|
|
3828
|
+
case "docs.sync":
|
|
3829
|
+
data = await referenceSync(text("version"), Boolean(parsed.dryRun));
|
|
3830
|
+
break;
|
|
3831
|
+
case "sandbox.up":
|
|
3832
|
+
case "sandbox.status":
|
|
3833
|
+
case "sandbox.down":
|
|
3834
|
+
data = await sandboxAction(operation.split(".")[1]);
|
|
3835
|
+
break;
|
|
3836
|
+
default: {
|
|
3837
|
+
const ctx = await loadProject(root);
|
|
3838
|
+
switch (operation) {
|
|
3839
|
+
case "compose.plan":
|
|
3840
|
+
data = await composePlan(ctx, schemas["compose.plan"].parse(parsed), oracle, signal);
|
|
3841
|
+
break;
|
|
3842
|
+
case "compose.materialize":
|
|
3843
|
+
data = await composeMaterialize(ctx, schemas["compose.materialize"].parse(parsed), signal);
|
|
3844
|
+
break;
|
|
3845
|
+
case "project.inspect":
|
|
3846
|
+
data = await projectInspect(ctx, parsed.detail);
|
|
3847
|
+
break;
|
|
3848
|
+
case "metadata.read": {
|
|
3849
|
+
await requireTrust(ctx.root);
|
|
3850
|
+
const env2 = environment(ctx, text("env"));
|
|
3851
|
+
const { project: _p, env: _e, ...request } = parsed;
|
|
3852
|
+
data = await metadataRead(oracle, env2, await resolveConnection(env2.readConnectionRef), request);
|
|
3853
|
+
break;
|
|
3854
|
+
}
|
|
3855
|
+
case "apex.generate": {
|
|
3856
|
+
await requireTrust(ctx.root);
|
|
3857
|
+
const generated = await oracle.generate(
|
|
3858
|
+
text("name"),
|
|
3859
|
+
text("alias") ?? ctx.config.application.alias
|
|
3860
|
+
);
|
|
3861
|
+
data = {
|
|
3862
|
+
...await installSources(generated.directory, ctx.root, text("output")),
|
|
3863
|
+
compiler: generated.compiler
|
|
3864
|
+
};
|
|
3865
|
+
break;
|
|
3866
|
+
}
|
|
3867
|
+
case "apex.sync":
|
|
3868
|
+
data = await deployment.sync(
|
|
3869
|
+
ctx,
|
|
3870
|
+
text("env"),
|
|
3871
|
+
parsed.action,
|
|
3872
|
+
signal
|
|
3873
|
+
);
|
|
3874
|
+
break;
|
|
3875
|
+
case "project.adopt":
|
|
3876
|
+
case "apex.export": {
|
|
3877
|
+
await requireTrust(ctx.root);
|
|
3878
|
+
const env2 = environment(ctx, text("env"));
|
|
3879
|
+
if (operation === "project.adopt" && env2.applicationId !== parsed.appId)
|
|
3880
|
+
throw new Fault(
|
|
3881
|
+
"APPLICATION_TARGET_MISMATCH",
|
|
3882
|
+
"Requested app ID differs from the environment mapping.",
|
|
3883
|
+
5
|
|
3884
|
+
);
|
|
3885
|
+
if (operation === "project.adopt" && parsed.workingCopy) {
|
|
3886
|
+
data = await deployment.sync(ctx, text("env"), "init", signal);
|
|
3887
|
+
break;
|
|
3888
|
+
}
|
|
3889
|
+
const connection = await resolveConnection(env2.readConnectionRef);
|
|
3890
|
+
await oracle.verifyTarget(env2, connection);
|
|
3891
|
+
const exported = await oracle.exportApplication(env2, connection);
|
|
3892
|
+
data = await installSources(
|
|
3893
|
+
exported.directory,
|
|
3894
|
+
ctx.root,
|
|
3895
|
+
operation === "project.adopt" ? ctx.config.application.sourceDir : text("output")
|
|
3896
|
+
);
|
|
3897
|
+
break;
|
|
3898
|
+
}
|
|
3899
|
+
case "apex.validate":
|
|
3900
|
+
await requireTrust(ctx.root);
|
|
3901
|
+
data = await validateApplication(
|
|
3902
|
+
oracle,
|
|
3903
|
+
await contained(ctx.root, ctx.config.application.sourceDir),
|
|
3904
|
+
parseDiagnostics,
|
|
3905
|
+
signal
|
|
3906
|
+
);
|
|
3907
|
+
break;
|
|
3908
|
+
case "ship": {
|
|
3909
|
+
const planned = await shipPlan(ctx, text("env"), deployment, parseDiagnostics, progress);
|
|
3910
|
+
if (parsed.mode !== "apply") {
|
|
3911
|
+
data = {
|
|
3912
|
+
mode: "plan",
|
|
3913
|
+
status: "planned",
|
|
3914
|
+
...planned.preview,
|
|
3915
|
+
planPath: planned.planPath,
|
|
3916
|
+
phases: planned.phases
|
|
3917
|
+
};
|
|
3918
|
+
break;
|
|
3919
|
+
}
|
|
3920
|
+
const applied = await shipApply(
|
|
3921
|
+
ctx,
|
|
3922
|
+
planned.plan,
|
|
3923
|
+
text("userRequest"),
|
|
3924
|
+
deployment,
|
|
3925
|
+
() => testReports.at(-1),
|
|
3926
|
+
signal,
|
|
3927
|
+
progress
|
|
3928
|
+
);
|
|
3929
|
+
data = {
|
|
3930
|
+
mode: "apply",
|
|
3931
|
+
...applied,
|
|
3932
|
+
planPath: planned.planPath,
|
|
3933
|
+
phases: [...planned.phases, ...applied.phases]
|
|
3934
|
+
};
|
|
3935
|
+
break;
|
|
3936
|
+
}
|
|
3937
|
+
case "ship.apply":
|
|
3938
|
+
data = await shipApply(
|
|
3939
|
+
ctx,
|
|
3940
|
+
await readJson(await contained(ctx.root, text("plan"))),
|
|
3941
|
+
text("userRequest"),
|
|
3942
|
+
deployment,
|
|
3943
|
+
() => testReports.at(-1),
|
|
3944
|
+
signal,
|
|
3945
|
+
progress
|
|
3946
|
+
);
|
|
3947
|
+
break;
|
|
3948
|
+
case "apex.diff": {
|
|
3949
|
+
await requireTrust(ctx.root);
|
|
3950
|
+
const env2 = environment(ctx, text("env"));
|
|
3951
|
+
const store = new SyncStore(ctx, env2, text("env"));
|
|
3952
|
+
const state = parsed.comparison === "live" ? null : await store.read();
|
|
3953
|
+
let exported, provenance;
|
|
3954
|
+
if (state && state.status !== "invalidated" && parsed.comparison !== "live") {
|
|
3955
|
+
await store.validate(state);
|
|
3956
|
+
exported = checkpoint(state);
|
|
3957
|
+
provenance = state.lastSuccessfulImport ? "last-successful-import" : "initial-baseline";
|
|
3958
|
+
} else {
|
|
3959
|
+
const connection = await resolveConnection(env2.readConnectionRef);
|
|
3960
|
+
await oracle.verifyTarget(env2, connection);
|
|
3961
|
+
exported = await oracle.exportApplication(env2, connection);
|
|
3962
|
+
provenance = "live-export";
|
|
3963
|
+
}
|
|
3964
|
+
const local = (await projectInspect(ctx)).sources.apex;
|
|
3965
|
+
data = {
|
|
3966
|
+
scope: "full-application-import",
|
|
3967
|
+
completeness: "textual-file-hashes-only",
|
|
3968
|
+
provenance,
|
|
3969
|
+
changes: [.../* @__PURE__ */ new Set([...Object.keys(exported.files), ...Object.keys(local ?? {})])].filter((f) => exported.files[f] !== local?.[f]).map((file) => ({
|
|
3970
|
+
file,
|
|
3971
|
+
before: exported.files[file] ?? null,
|
|
3972
|
+
after: local?.[file] ?? null
|
|
3973
|
+
}))
|
|
3974
|
+
};
|
|
3975
|
+
break;
|
|
3976
|
+
}
|
|
3977
|
+
case "db.plan":
|
|
3978
|
+
data = await deployment.plan(ctx, text("env"));
|
|
3979
|
+
break;
|
|
3980
|
+
case "deploy.plan": {
|
|
3981
|
+
const plan = await deployment.plan(ctx, text("env"));
|
|
3982
|
+
await writeJson(await contained(ctx.root, text("out")), plan);
|
|
3983
|
+
data = plan;
|
|
3984
|
+
break;
|
|
3985
|
+
}
|
|
3986
|
+
case "deploy.apply":
|
|
3987
|
+
data = await deployment.apply(
|
|
3988
|
+
ctx,
|
|
3989
|
+
await readJson(await contained(ctx.root, text("plan"))),
|
|
3990
|
+
signal
|
|
3991
|
+
);
|
|
3992
|
+
break;
|
|
3993
|
+
case "deploy.status":
|
|
3994
|
+
await requireTrust(ctx.root);
|
|
3995
|
+
data = await deployment.reconcile(ctx, text("run"));
|
|
3996
|
+
break;
|
|
3997
|
+
case "deploy.restore-plan": {
|
|
3998
|
+
await requireTrust(ctx.root);
|
|
3999
|
+
const plan = await deployment.restorePlan(ctx, text("backup"));
|
|
4000
|
+
await writeJson(await contained(ctx.root, text("out")), plan);
|
|
4001
|
+
data = plan;
|
|
4002
|
+
break;
|
|
4003
|
+
}
|
|
4004
|
+
case "test.run": {
|
|
4005
|
+
const suite = text("suite");
|
|
4006
|
+
if (suite === "all") {
|
|
4007
|
+
if (!parsed.env) throw new Fault("ENVIRONMENT_REQUIRED", "test all requires --env.", 2);
|
|
4008
|
+
const result = await tests.all(ctx, text("env"), signal);
|
|
4009
|
+
data = result.data;
|
|
4010
|
+
if (!result.ok)
|
|
4011
|
+
return {
|
|
4012
|
+
...failure(operation, new Fault("QUALITY_GATE_FAILED", "Required suites did not pass.", 1)),
|
|
4013
|
+
data
|
|
4014
|
+
};
|
|
4015
|
+
} else {
|
|
4016
|
+
const result = await tests.run(
|
|
4017
|
+
ctx,
|
|
4018
|
+
suite,
|
|
4019
|
+
text("env"),
|
|
4020
|
+
signal,
|
|
4021
|
+
Boolean(parsed.headed)
|
|
4022
|
+
);
|
|
4023
|
+
data = result;
|
|
4024
|
+
if (result.status !== "passed")
|
|
4025
|
+
return {
|
|
4026
|
+
...failure(
|
|
4027
|
+
operation,
|
|
4028
|
+
new Fault(
|
|
4029
|
+
"TEST_" + result.status.toUpperCase(),
|
|
4030
|
+
result.diagnostic ?? `Suite is ${result.status}.`,
|
|
4031
|
+
result.status === "failed" ? 1 : 3,
|
|
4032
|
+
result.status
|
|
4033
|
+
)
|
|
4034
|
+
),
|
|
4035
|
+
data
|
|
4036
|
+
};
|
|
4037
|
+
}
|
|
4038
|
+
break;
|
|
4039
|
+
}
|
|
4040
|
+
case "test.report":
|
|
4041
|
+
data = await readJson(await contained(ctx.root, ".apexrest/test-runs/" + text("run") + ".json"));
|
|
4042
|
+
break;
|
|
4043
|
+
case "test.auth":
|
|
4044
|
+
data = await tests.auth(ctx, text("env"));
|
|
4045
|
+
break;
|
|
4046
|
+
case "browser.open": {
|
|
4047
|
+
const { openVerificationBrowser } = await import("./chunk-CVMECW6D.mjs");
|
|
4048
|
+
data = await openVerificationBrowser(
|
|
4049
|
+
ctx,
|
|
4050
|
+
text("env"),
|
|
4051
|
+
void 0,
|
|
4052
|
+
parsed.browserMode
|
|
4053
|
+
);
|
|
4054
|
+
break;
|
|
4055
|
+
}
|
|
4056
|
+
case "jobs.status":
|
|
4057
|
+
data = await new JobService(ctx).status(text("id"), Number(parsed.waitSeconds), signal);
|
|
4058
|
+
break;
|
|
4059
|
+
case "jobs.cancel":
|
|
4060
|
+
data = await new JobService(ctx).cancel(text("id"));
|
|
4061
|
+
break;
|
|
4062
|
+
case "artifacts.read":
|
|
4063
|
+
data = await new ArtifactService(ctx).read(
|
|
4064
|
+
text("id"),
|
|
4065
|
+
Number(parsed.offset),
|
|
4066
|
+
Number(parsed.limit)
|
|
4067
|
+
);
|
|
4068
|
+
break;
|
|
4069
|
+
default:
|
|
4070
|
+
throw new Fault("INVALID_INPUT", "Unknown operation.", 2);
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
return success(operation, data);
|
|
4075
|
+
} catch (error) {
|
|
4076
|
+
return failure(operation, error);
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
export {
|
|
4081
|
+
JobService,
|
|
4082
|
+
settleInlineJobs,
|
|
4083
|
+
failQueuedJob,
|
|
4084
|
+
executeJob,
|
|
4085
|
+
schemas,
|
|
4086
|
+
internalOperations,
|
|
4087
|
+
toolCatalog,
|
|
4088
|
+
shutdownOracle,
|
|
4089
|
+
dispatch
|
|
4090
|
+
};
|