@databricks/appkit 0.71.0 → 0.73.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/CLAUDE.md +63 -0
- package/NOTICE.md +3 -2
- package/dist/appkit/package.js +1 -1
- package/dist/beta.d.ts +18 -3
- package/dist/beta.js +14 -1
- package/dist/cli/commands/agent/eval.js +120 -0
- package/dist/cli/commands/agent/eval.js.map +1 -0
- package/dist/cli/commands/agent/index.js +18 -0
- package/dist/cli/commands/agent/index.js.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/connectors/index.js +2 -0
- package/dist/connectors/mlflow/auth.d.ts +28 -0
- package/dist/connectors/mlflow/auth.d.ts.map +1 -0
- package/dist/connectors/mlflow/auth.js +70 -0
- package/dist/connectors/mlflow/auth.js.map +1 -0
- package/dist/connectors/mlflow/client.d.ts +51 -0
- package/dist/connectors/mlflow/client.d.ts.map +1 -0
- package/dist/connectors/mlflow/client.js +93 -0
- package/dist/connectors/mlflow/client.js.map +1 -0
- package/dist/connectors/mlflow/index.d.ts +2 -0
- package/dist/database/errors.js +15 -5
- package/dist/database/errors.js.map +1 -1
- package/dist/database/runtime/data-path.d.ts +7 -0
- package/dist/database/runtime/data-path.d.ts.map +1 -0
- package/dist/database/runtime/data-path.js.map +1 -1
- package/dist/database/runtime/engine/drizzle-data-path.js +7 -5
- package/dist/database/runtime/engine/drizzle-data-path.js.map +1 -1
- package/dist/database/schema-builder/define-schema.d.ts +1 -1
- package/dist/database/schema-builder/define-schema.js +1 -1
- package/dist/database/schema-builder/define-schema.js.map +1 -1
- package/dist/errors/database-validation.d.ts +23 -0
- package/dist/errors/database-validation.d.ts.map +1 -0
- package/dist/errors/database-validation.js +24 -0
- package/dist/errors/database-validation.js.map +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/evals/dataset.d.ts +36 -0
- package/dist/evals/dataset.d.ts.map +1 -0
- package/dist/evals/dataset.js +36 -0
- package/dist/evals/dataset.js.map +1 -0
- package/dist/evals/define-eval.d.ts +26 -0
- package/dist/evals/define-eval.d.ts.map +1 -0
- package/dist/evals/define-eval.js +28 -0
- package/dist/evals/define-eval.js.map +1 -0
- package/dist/evals/discover.d.ts +20 -0
- package/dist/evals/discover.d.ts.map +1 -0
- package/dist/evals/discover.js +49 -0
- package/dist/evals/discover.js.map +1 -0
- package/dist/evals/http-driver.d.ts +33 -0
- package/dist/evals/http-driver.d.ts.map +1 -0
- package/dist/evals/http-driver.js +123 -0
- package/dist/evals/http-driver.js.map +1 -0
- package/dist/evals/index.d.ts +14 -0
- package/dist/evals/index.js +14 -0
- package/dist/evals/judge.d.ts +27 -0
- package/dist/evals/judge.d.ts.map +1 -0
- package/dist/evals/judge.js +77 -0
- package/dist/evals/judge.js.map +1 -0
- package/dist/evals/matchers.d.ts +12 -0
- package/dist/evals/matchers.d.ts.map +1 -0
- package/dist/evals/matchers.js +26 -0
- package/dist/evals/matchers.js.map +1 -0
- package/dist/evals/mlflow-report.d.ts +37 -0
- package/dist/evals/mlflow-report.d.ts.map +1 -0
- package/dist/evals/mlflow-report.js +161 -0
- package/dist/evals/mlflow-report.js.map +1 -0
- package/dist/evals/mlflow-run.d.ts +13 -0
- package/dist/evals/mlflow-run.d.ts.map +1 -0
- package/dist/evals/mlflow-run.js +101 -0
- package/dist/evals/mlflow-run.js.map +1 -0
- package/dist/evals/pool.js +24 -0
- package/dist/evals/pool.js.map +1 -0
- package/dist/evals/report.d.ts +25 -0
- package/dist/evals/report.d.ts.map +1 -0
- package/dist/evals/report.js +57 -0
- package/dist/evals/report.js.map +1 -0
- package/dist/evals/run-eval.d.ts +23 -0
- package/dist/evals/run-eval.d.ts.map +1 -0
- package/dist/evals/run-eval.js +152 -0
- package/dist/evals/run-eval.js.map +1 -0
- package/dist/evals/run-evals.d.ts +94 -0
- package/dist/evals/run-evals.d.ts.map +1 -0
- package/dist/evals/run-evals.js +257 -0
- package/dist/evals/run-evals.js.map +1 -0
- package/dist/evals/types.d.ts +163 -0
- package/dist/evals/types.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +1 -1
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/agents/agents.js +1 -1
- package/dist/plugins/database/crud/contract.js +17 -8
- package/dist/plugins/database/crud/contract.js.map +1 -1
- package/dist/plugins/database/crud/exposure.js +63 -22
- package/dist/plugins/database/crud/exposure.js.map +1 -1
- package/dist/plugins/database/crud/request.js +50 -0
- package/dist/plugins/database/crud/request.js.map +1 -0
- package/dist/plugins/database/crud/response.js +77 -0
- package/dist/plugins/database/crud/response.js.map +1 -0
- package/dist/plugins/database/crud/routes.js +71 -52
- package/dist/plugins/database/crud/routes.js.map +1 -1
- package/dist/plugins/database/database.d.ts +6 -4
- package/dist/plugins/database/database.d.ts.map +1 -1
- package/dist/plugins/database/database.js +46 -16
- package/dist/plugins/database/database.js.map +1 -1
- package/dist/plugins/database/defaults.js +5 -1
- package/dist/plugins/database/defaults.js.map +1 -1
- package/dist/plugins/database/entity-client.js +143 -10
- package/dist/plugins/database/entity-client.js.map +1 -1
- package/dist/plugins/database/entity-types.d.ts +1 -1
- package/dist/plugins/database/hooks.d.ts +38 -0
- package/dist/plugins/database/hooks.d.ts.map +1 -0
- package/dist/plugins/database/index.d.ts +3 -2
- package/dist/plugins/database/lifecycle.js +67 -28
- package/dist/plugins/database/lifecycle.js.map +1 -1
- package/dist/plugins/database/scope.js +58 -0
- package/dist/plugins/database/scope.js.map +1 -0
- package/dist/plugins/database/types.d.ts +40 -12
- package/dist/plugins/database/types.d.ts.map +1 -1
- package/docs/api/appkit/Class.AppKitError.md +1 -0
- package/docs/api/appkit/Class.DatabaseValidationError.md +191 -0
- package/docs/api/appkit/Class.MlflowClient.md +103 -0
- package/docs/api/appkit/Function.buildAssessments.md +16 -0
- package/docs/api/appkit/Function.configureJudge.md +18 -0
- package/docs/api/appkit/Function.createHttpDriver.md +18 -0
- package/docs/api/appkit/Function.defineEval.md +35 -0
- package/docs/api/appkit/Function.defineSchema.md +1 -1
- package/docs/api/appkit/Function.discoverEvalFiles.md +18 -0
- package/docs/api/appkit/Function.equals.md +18 -0
- package/docs/api/appkit/Function.evalGlyph.md +18 -0
- package/docs/api/appkit/Function.formatEvalDetail.md +18 -0
- package/docs/api/appkit/Function.formatEvalHeadline.md +18 -0
- package/docs/api/appkit/Function.formatEvalResults.md +18 -0
- package/docs/api/appkit/Function.formatSummaryLine.md +18 -0
- package/docs/api/appkit/Function.includes.md +18 -0
- package/docs/api/appkit/Function.isJudgeConfigured.md +10 -0
- package/docs/api/appkit/Function.matches.md +18 -0
- package/docs/api/appkit/Function.normalizeHost.md +18 -0
- package/docs/api/appkit/Function.readEvalDataset.md +21 -0
- package/docs/api/appkit/Function.reportToMlflow.md +23 -0
- package/docs/api/appkit/Function.resolveDatabricksAuth.md +16 -0
- package/docs/api/appkit/Function.resolveWorkspaceClient.md +18 -0
- package/docs/api/appkit/Function.runEval.md +19 -0
- package/docs/api/appkit/Function.runEvalsInDir.md +18 -0
- package/docs/api/appkit/Function.summarize.md +16 -0
- package/docs/api/appkit/Interface.AssertionHandle.md +54 -0
- package/docs/api/appkit/Interface.AssertionResult.md +48 -0
- package/docs/api/appkit/Interface.Assessment.md +83 -0
- package/docs/api/appkit/Interface.CustomJudgeSpec.md +30 -0
- package/docs/api/appkit/Interface.DatabaseValidationIssue.md +21 -0
- package/docs/api/appkit/Interface.DatabricksAuth.md +21 -0
- package/docs/api/appkit/Interface.DatasetRow.md +21 -0
- package/docs/api/appkit/Interface.DiscoveredEval.md +36 -0
- package/docs/api/appkit/Interface.DriveResult.md +58 -0
- package/docs/api/appkit/Interface.EntityMutationHooks.md +173 -0
- package/docs/api/appkit/Interface.EvalDefinition.md +74 -0
- package/docs/api/appkit/Interface.EvalDriver.md +37 -0
- package/docs/api/appkit/Interface.EvalResult.md +83 -0
- package/docs/api/appkit/Interface.EvalRunSummary.md +46 -0
- package/docs/api/appkit/Interface.EvalSummary.md +48 -0
- package/docs/api/appkit/Interface.HookApp.md +12 -0
- package/docs/api/appkit/Interface.HookContext.md +21 -0
- package/docs/api/appkit/Interface.HttpDriverOptions.md +67 -0
- package/docs/api/appkit/Interface.JudgeConfig.md +34 -0
- package/docs/api/appkit/Interface.JudgeScore.md +21 -0
- package/docs/api/appkit/Interface.MatchResult.md +34 -0
- package/docs/api/appkit/Interface.PostResult.md +30 -0
- package/docs/api/appkit/Interface.ReadEvalDatasetOptions.md +34 -0
- package/docs/api/appkit/Interface.ReadSerializerContext.md +21 -0
- package/docs/api/appkit/Interface.ReportOutcome.md +53 -0
- package/docs/api/appkit/Interface.ResolveDatabricksAuthOptions.md +34 -0
- package/docs/api/appkit/Interface.RunEvalOptions.md +45 -0
- package/docs/api/appkit/Interface.RunEvalsOptions.md +214 -0
- package/docs/api/appkit/Interface.TestContext.md +245 -0
- package/docs/api/appkit/TypeAlias.DatabaseApiConfig.md +53 -0
- package/docs/api/appkit/TypeAlias.DatabaseApiWriteOperation.md +8 -0
- package/docs/api/appkit/TypeAlias.DatabaseApiWritesConfig.md +49 -0
- package/docs/api/appkit/TypeAlias.DatabaseExports.md +3 -3
- package/docs/api/appkit/TypeAlias.EntityHooks.md +25 -0
- package/docs/api/appkit/TypeAlias.EvalProgress.md +26 -0
- package/docs/api/appkit/TypeAlias.IDatabaseConfig.md +16 -5
- package/docs/api/appkit/TypeAlias.Matcher.md +18 -0
- package/docs/api/appkit/TypeAlias.ReadSerializer.md +19 -0
- package/docs/api/appkit/TypeAlias.Severity.md +8 -0
- package/docs/api/appkit/TypeAlias.TransactionClient.md +19 -0
- package/docs/api/appkit.md +157 -95
- package/docs/plugins/database.md +144 -0
- package/llms.txt +63 -0
- package/package.json +3 -2
- package/sbom.cdx.json +1 -1
|
@@ -13,6 +13,10 @@ const databaseWriteDefaults = {
|
|
|
13
13
|
* connection past the deadline.
|
|
14
14
|
*/
|
|
15
15
|
const STATEMENT_TIMEOUT_MS = 3e4;
|
|
16
|
+
/** Server-side limit for an idle open transaction, independent of JS timers (ms). */
|
|
17
|
+
const IDLE_IN_TRANSACTION_TIMEOUT_MS = 3e4;
|
|
18
|
+
/** Deadline for the transaction callback, not cancellation of arbitrary hook work (ms). */
|
|
19
|
+
const TRANSACTION_TIMEOUT_MS = 3e4;
|
|
16
20
|
/** Max encoded size of a generated read query string. */
|
|
17
21
|
const MAX_QUERY_BYTES = 8 * 1024;
|
|
18
22
|
/** Max nesting depth of `and`/`or` groups in one generated filter. */
|
|
@@ -35,5 +39,5 @@ const MAX_SERIALIZED_NODES = 1e4;
|
|
|
35
39
|
const MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
36
40
|
|
|
37
41
|
//#endregion
|
|
38
|
-
export { MAX_GROUP_ITEMS, MAX_MATERIALIZED_NODES, MAX_OFFSET, MAX_ORDER_FIELDS, MAX_QUERY_BYTES, MAX_RESPONSE_BYTES, MAX_SERIALIZED_DEPTH, MAX_SERIALIZED_NODES, MAX_WHERE_CONDITIONS, MAX_WHERE_DEPTH, STATEMENT_TIMEOUT_MS, databaseReadDefaults, databaseWriteDefaults };
|
|
42
|
+
export { IDLE_IN_TRANSACTION_TIMEOUT_MS, MAX_GROUP_ITEMS, MAX_MATERIALIZED_NODES, MAX_OFFSET, MAX_ORDER_FIELDS, MAX_QUERY_BYTES, MAX_RESPONSE_BYTES, MAX_SERIALIZED_DEPTH, MAX_SERIALIZED_NODES, MAX_WHERE_CONDITIONS, MAX_WHERE_DEPTH, STATEMENT_TIMEOUT_MS, TRANSACTION_TIMEOUT_MS, databaseReadDefaults, databaseWriteDefaults };
|
|
39
43
|
//# sourceMappingURL=defaults.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaults.js","names":[],"sources":["../../../src/plugins/database/defaults.ts"],"sourcesContent":["import type { PluginExecuteConfig } from \"shared\";\n\n/** Default interceptor policy for bounded reads. */\nexport const databaseReadDefaults: PluginExecuteConfig = {\n retry: { enabled: false },\n};\n\n/** Default interceptor policy for mutations. */\nexport const databaseWriteDefaults: PluginExecuteConfig = {\n cache: { enabled: false },\n retry: { enabled: false },\n};\n\n// Connection posture: how long any one statement may hold a pooled connection.\n/**\n * Server-side `statement_timeout` applied to every pooled connection (ms).\n * PostgreSQL cancels the statement itself, so an expensive caller-supplied\n * filter (for example a leading-wildcard `ilike`) cannot hold its pooled\n * connection past the deadline.\n */\nexport const STATEMENT_TIMEOUT_MS = 30_000;\n\n// Generated-read limits. The wire caps a typed caller shares with HTTP live in\n// `database/contract`; these bound only what an untrusted request may ask for.\n\n// Request decoding: rejected before any database work is scheduled.\n/** Max encoded size of a generated read query string. */\nexport const MAX_QUERY_BYTES = 8 * 1024;\n/** Max nesting depth of `and`/`or` groups in one generated filter. */\nexport const MAX_WHERE_DEPTH = 5;\n/** Max column conditions across one generated filter. */\nexport const MAX_WHERE_CONDITIONS = 50;\n/** Max members in one `and`/`or` group. */\nexport const MAX_GROUP_ITEMS = 20;\n/** Max columns one generated `order` may name. */\nexport const MAX_ORDER_FIELDS = 10;\n/** Max accepted generated `offset`. */\nexport const MAX_OFFSET = 10_000;\n/** Max rows one generated read may materialize across its include tree. */\nexport const MAX_MATERIALIZED_NODES = 10_000;\n\n// Response shaping: bounds trusted output on its way to the wire.\n/** Max nesting depth of read-serializer output. */\nexport const MAX_SERIALIZED_DEPTH = 32;\n/** Max node count of read-serializer output. */\nexport const MAX_SERIALIZED_NODES = 10_000;\n/** Max UTF-8 byte length of one generated read response body. */\nexport const MAX_RESPONSE_BYTES = 5 * 1024 * 1024;\n"],"mappings":";;AAGA,MAAa,uBAA4C,EACvD,OAAO,EAAE,SAAS,OAAO,EAC1B;;AAGD,MAAa,wBAA6C;CACxD,OAAO,EAAE,SAAS,OAAO;CACzB,OAAO,EAAE,SAAS,OAAO;CAC1B;;;;;;;AASD,MAAa,uBAAuB;;
|
|
1
|
+
{"version":3,"file":"defaults.js","names":[],"sources":["../../../src/plugins/database/defaults.ts"],"sourcesContent":["import type { PluginExecuteConfig } from \"shared\";\n\n/** Default interceptor policy for bounded reads. */\nexport const databaseReadDefaults: PluginExecuteConfig = {\n retry: { enabled: false },\n};\n\n/** Default interceptor policy for mutations. */\nexport const databaseWriteDefaults: PluginExecuteConfig = {\n cache: { enabled: false },\n retry: { enabled: false },\n};\n\n// Connection posture: how long any one statement may hold a pooled connection.\n/**\n * Server-side `statement_timeout` applied to every pooled connection (ms).\n * PostgreSQL cancels the statement itself, so an expensive caller-supplied\n * filter (for example a leading-wildcard `ilike`) cannot hold its pooled\n * connection past the deadline.\n */\nexport const STATEMENT_TIMEOUT_MS = 30_000;\n\n/** Server-side limit for an idle open transaction, independent of JS timers (ms). */\nexport const IDLE_IN_TRANSACTION_TIMEOUT_MS = 30_000;\n\n/** Deadline for the transaction callback, not cancellation of arbitrary hook work (ms). */\nexport const TRANSACTION_TIMEOUT_MS = 30_000;\n\n// Generated-read limits. The wire caps a typed caller shares with HTTP live in\n// `database/contract`; these bound only what an untrusted request may ask for.\n\n// Request decoding: rejected before any database work is scheduled.\n/** Max encoded size of a generated read query string. */\nexport const MAX_QUERY_BYTES = 8 * 1024;\n/** Max nesting depth of `and`/`or` groups in one generated filter. */\nexport const MAX_WHERE_DEPTH = 5;\n/** Max column conditions across one generated filter. */\nexport const MAX_WHERE_CONDITIONS = 50;\n/** Max members in one `and`/`or` group. */\nexport const MAX_GROUP_ITEMS = 20;\n/** Max columns one generated `order` may name. */\nexport const MAX_ORDER_FIELDS = 10;\n/** Max accepted generated `offset`. */\nexport const MAX_OFFSET = 10_000;\n/** Max rows one generated read may materialize across its include tree. */\nexport const MAX_MATERIALIZED_NODES = 10_000;\n\n// Response shaping: bounds trusted output on its way to the wire.\n/** Max nesting depth of read-serializer output. */\nexport const MAX_SERIALIZED_DEPTH = 32;\n/** Max node count of read-serializer output. */\nexport const MAX_SERIALIZED_NODES = 10_000;\n/** Max UTF-8 byte length of one generated read response body. */\nexport const MAX_RESPONSE_BYTES = 5 * 1024 * 1024;\n"],"mappings":";;AAGA,MAAa,uBAA4C,EACvD,OAAO,EAAE,SAAS,OAAO,EAC1B;;AAGD,MAAa,wBAA6C;CACxD,OAAO,EAAE,SAAS,OAAO;CACzB,OAAO,EAAE,SAAS,OAAO;CAC1B;;;;;;;AASD,MAAa,uBAAuB;;AAGpC,MAAa,iCAAiC;;AAG9C,MAAa,yBAAyB;;AAOtC,MAAa,kBAAkB,IAAI;;AAEnC,MAAa,kBAAkB;;AAE/B,MAAa,uBAAuB;;AAEpC,MAAa,kBAAkB;;AAE/B,MAAa,mBAAmB;;AAEhC,MAAa,aAAa;;AAE1B,MAAa,yBAAyB;;AAItC,MAAa,uBAAuB;;AAEpC,MAAa,uBAAuB;;AAEpC,MAAa,qBAAqB,IAAI,OAAO"}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DatabaseValidationError } from "../../errors/database-validation.js";
|
|
2
|
+
import "../../errors/index.js";
|
|
3
|
+
import { DatabasePluginError, classifyDatabaseError, databaseErrorFromStatus, invalidDatabaseRequest } from "../../database/errors.js";
|
|
2
4
|
import { databaseReadDefaults, databaseWriteDefaults } from "./defaults.js";
|
|
3
5
|
import { andWhere, validateLimit, validateOffset } from "../../database/runtime/data-path.js";
|
|
4
6
|
|
|
5
7
|
//#region src/plugins/database/entity-client.ts
|
|
8
|
+
/** Internal rollback signal for a hooked keyed mutation that matched no row. */
|
|
9
|
+
var MutationNoMatchError = class extends DatabasePluginError {
|
|
10
|
+
constructor() {
|
|
11
|
+
super("NOT_FOUND", "write");
|
|
12
|
+
}
|
|
13
|
+
};
|
|
6
14
|
/** Clone plain acyclic input so caller mutation cannot change a built query. */
|
|
7
15
|
function snapshotPlain(value, seen = /* @__PURE__ */ new Set()) {
|
|
8
16
|
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "number" && Number.isFinite(value)) return value;
|
|
@@ -22,6 +30,10 @@ function validateReadInput(validate) {
|
|
|
22
30
|
throw classifyDatabaseError(error, "read");
|
|
23
31
|
}
|
|
24
32
|
}
|
|
33
|
+
/** A hook replaced the payload only if it returned an object to persist. */
|
|
34
|
+
function isReplacement(value) {
|
|
35
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
25
37
|
/**
|
|
26
38
|
* Plugin-facing client that composes immutable query state. The DataPath remains
|
|
27
39
|
* the schema-aware validation and execution boundary.
|
|
@@ -99,27 +111,148 @@ var EntityClient = class EntityClient {
|
|
|
99
111
|
count() {
|
|
100
112
|
return this.run("read", (dataPath) => dataPath.count(this.ctx.table, this.state.where));
|
|
101
113
|
}
|
|
102
|
-
create(values) {
|
|
103
|
-
|
|
114
|
+
async create(values) {
|
|
115
|
+
this.assertUnfiltered("create");
|
|
116
|
+
const hooks = this.ctx.hooks;
|
|
117
|
+
const insert = (payload, byHook = false) => this.write("create", payload, (dataPath, parsed) => dataPath.insert(this.ctx.table, parsed), byHook);
|
|
118
|
+
if (this.isDirect("create")) return this.directMutation("create", () => insert(values));
|
|
119
|
+
return this.mutate("create", (entity) => entity.create(values), async (context) => {
|
|
120
|
+
const payload = await this.runBefore(() => hooks?.beforeCreate?.(values, context), values);
|
|
121
|
+
const row = await insert(payload, payload !== values);
|
|
122
|
+
await this.runHook(() => hooks?.afterCreate?.(row, context));
|
|
123
|
+
return row;
|
|
124
|
+
});
|
|
104
125
|
}
|
|
105
126
|
update(id, values) {
|
|
106
|
-
return this.
|
|
127
|
+
return this.updateWithNoMatch(id, values, new MutationNoMatchError());
|
|
128
|
+
}
|
|
129
|
+
async updateWithNoMatch(id, values, noMatch) {
|
|
130
|
+
const hooks = this.ctx.hooks;
|
|
131
|
+
const apply = (payload, byHook = false) => this.write("update", payload, (dataPath, parsed) => dataPath.update(this.ctx.table, id, parsed, this.state.where), byHook);
|
|
132
|
+
if (this.isDirect("update")) return this.directMutation("update", () => apply(values));
|
|
133
|
+
try {
|
|
134
|
+
return await this.mutate("update", (entity) => entity.updateWithNoMatch(id, values, noMatch), async (context) => {
|
|
135
|
+
const payload = await this.runBefore(() => hooks?.beforeUpdate?.(id, values, context), values);
|
|
136
|
+
const row = await apply(payload, payload !== values);
|
|
137
|
+
if (row === null) throw noMatch;
|
|
138
|
+
await this.runHook(() => hooks?.afterUpdate?.(row, context));
|
|
139
|
+
return row;
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error === noMatch && !this.ctx.transactionBound && !this.ctx.scope.activeTransaction()) return null;
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
107
145
|
}
|
|
108
|
-
upsert(values, options) {
|
|
146
|
+
async upsert(values, options) {
|
|
147
|
+
this.assertUnfiltered("upsert");
|
|
148
|
+
const hooks = this.ctx.hooks;
|
|
109
149
|
const onConflict = options.onConflict;
|
|
110
|
-
|
|
150
|
+
const apply = (payload, byHook = false) => this.write("create", payload, (dataPath, parsed) => dataPath.upsert(this.ctx.table, parsed, onConflict), byHook);
|
|
151
|
+
if (this.isDirect("upsert")) return this.directMutation("upsert", () => apply(values));
|
|
152
|
+
return this.mutate("upsert", (entity) => entity.upsert(values, { onConflict }), async (context) => {
|
|
153
|
+
const payload = await this.runBefore(() => hooks?.beforeUpsert?.(values, context), values);
|
|
154
|
+
const row = await apply(payload, payload !== values);
|
|
155
|
+
await this.runHook(() => hooks?.afterUpsert?.(row, context));
|
|
156
|
+
return row;
|
|
157
|
+
});
|
|
111
158
|
}
|
|
112
159
|
delete(id) {
|
|
113
|
-
return this.
|
|
160
|
+
return this.deleteWithNoMatch(id, new MutationNoMatchError());
|
|
161
|
+
}
|
|
162
|
+
async deleteWithNoMatch(id, noMatch) {
|
|
163
|
+
const hooks = this.ctx.hooks;
|
|
164
|
+
const remove = () => this.run("write", (dataPath) => dataPath.delete(this.ctx.table, id, this.state.where), databaseWriteDefaults);
|
|
165
|
+
if (this.isDirect("delete")) return this.directMutation("delete", remove);
|
|
166
|
+
try {
|
|
167
|
+
return await this.mutate("delete", (entity) => entity.deleteWithNoMatch(id, noMatch), async (context) => {
|
|
168
|
+
await this.runHook(() => hooks?.beforeDelete?.(id, context));
|
|
169
|
+
if (!await remove()) throw noMatch;
|
|
170
|
+
await this.runHook(() => hooks?.afterDelete?.(id, context));
|
|
171
|
+
return true;
|
|
172
|
+
});
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error === noMatch && !this.ctx.transactionBound && !this.ctx.scope.activeTransaction()) return false;
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Count every write that runs on an already bound transaction. */
|
|
179
|
+
directMutation(operation, run) {
|
|
180
|
+
if (!this.ctx.transactionBound) return run();
|
|
181
|
+
return this.ctx.scope.runMutation(this.ctx.table.$name, operation, run);
|
|
182
|
+
}
|
|
183
|
+
/** An insert matches no existing row, so a predicate could only be dropped. */
|
|
184
|
+
assertUnfiltered(operation) {
|
|
185
|
+
if (this.state.where !== void 0) throw invalidDatabaseRequest(`${operation}() cannot follow where(); it does not select rows to change`);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Go straight to the DataPath only when there is nothing to sequence and no
|
|
189
|
+
* open transaction to join. A root client that skipped the second check would
|
|
190
|
+
* commit on the pool while a transaction it cannot see is still open.
|
|
191
|
+
*/
|
|
192
|
+
isDirect(operation) {
|
|
193
|
+
if (this.hasHooks(operation)) return false;
|
|
194
|
+
return this.ctx.transactionBound || !this.ctx.scope.activeTransaction();
|
|
195
|
+
}
|
|
196
|
+
hasHooks(operation) {
|
|
197
|
+
const hooks = this.ctx.hooks;
|
|
198
|
+
if (!hooks) return false;
|
|
199
|
+
switch (operation) {
|
|
200
|
+
case "create": return Boolean(hooks.beforeCreate ?? hooks.afterCreate);
|
|
201
|
+
case "update": return Boolean(hooks.beforeUpdate ?? hooks.afterUpdate);
|
|
202
|
+
case "upsert": return Boolean(hooks.beforeUpsert ?? hooks.afterUpsert);
|
|
203
|
+
case "delete": return Boolean(hooks.beforeDelete ?? hooks.afterDelete);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Run one mutation atomically. A top-level call restarts itself on the
|
|
208
|
+
* transaction-bound client, so hooks, validation, the mutation, and its after
|
|
209
|
+
* hook commit or roll back together. An unhooked root mutation reaches this
|
|
210
|
+
* only to join an open transaction; its bound client counts the direct write
|
|
211
|
+
* without constructing a hook context.
|
|
212
|
+
*/
|
|
213
|
+
async mutate(operation, restart, body) {
|
|
214
|
+
if (this.ctx.transactionBound) return this.ctx.scope.runMutation(this.ctx.table.$name, operation, () => body(this.hookContext()));
|
|
215
|
+
try {
|
|
216
|
+
return await this.ctx.runInTransaction((entity) => restart(entity.clone(this.state)));
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (error instanceof DatabaseValidationError || error instanceof MutationNoMatchError) throw error;
|
|
219
|
+
throw classifyDatabaseError(error, "write");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** Resolve the surface at hook time so it cannot outlive its transaction. */
|
|
223
|
+
hookContext() {
|
|
224
|
+
const database = this.ctx.scope.activeTransaction();
|
|
225
|
+
if (!database) throw new DatabasePluginError("INTERNAL", "write");
|
|
226
|
+
return {
|
|
227
|
+
entity: this.ctx.table.$name,
|
|
228
|
+
app: { database }
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** Substitute the payload a before hook returned, if it returned one. */
|
|
232
|
+
async runBefore(invoke, values) {
|
|
233
|
+
const replacement = await this.runHook(invoke);
|
|
234
|
+
if (replacement === void 0 || replacement === null) return values;
|
|
235
|
+
if (!isReplacement(replacement)) throw new DatabasePluginError("INTERNAL", "write");
|
|
236
|
+
return replacement;
|
|
237
|
+
}
|
|
238
|
+
/** Keep a deliberate validation signal; collapse every other hook failure. */
|
|
239
|
+
async runHook(invoke) {
|
|
240
|
+
try {
|
|
241
|
+
return await invoke();
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error instanceof DatabaseValidationError) throw error;
|
|
244
|
+
throw classifyDatabaseError(error, "write");
|
|
245
|
+
}
|
|
114
246
|
}
|
|
115
247
|
/** Validate caller values against the finalized trusted-write schema. */
|
|
116
|
-
async write(kind, values, operation) {
|
|
117
|
-
|
|
248
|
+
async write(kind, values, operation, byHook = false) {
|
|
249
|
+
const rejection = byHook ? "INTERNAL" : "INVALID_REQUEST";
|
|
250
|
+
if (kind === "update" && Object.keys(values).length === 0) throw new DatabasePluginError(rejection, "write");
|
|
118
251
|
let parsed;
|
|
119
252
|
try {
|
|
120
253
|
parsed = (kind === "create" ? this.ctx.table.$insertSchema : this.ctx.table.$updateSchema).parse(values);
|
|
121
254
|
} catch {
|
|
122
|
-
throw new DatabasePluginError(
|
|
255
|
+
throw new DatabasePluginError(rejection, "write");
|
|
123
256
|
}
|
|
124
257
|
return this.run("write", (dataPath) => operation(dataPath, parsed), databaseWriteDefaults);
|
|
125
258
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-client.js","names":[],"sources":["../../../src/plugins/database/entity-client.ts"],"sourcesContent":["import type { PluginExecuteConfig } from \"shared\";\n\nimport {\n classifyDatabaseError,\n DatabasePluginError,\n databaseErrorFromStatus,\n} from \"../../database/errors\";\nimport type {\n DataPath,\n IdValue,\n IncludeSpec,\n OrderSpec,\n QuerySpec,\n Row,\n WhereClause,\n} from \"../../database/runtime\";\nimport {\n andWhere,\n validateLimit,\n validateOffset,\n} from \"../../database/runtime/data-path\";\nimport type { AppKitTable } from \"../../database/schema-builder\";\nimport type { ExecutionResult } from \"../../plugin/execution-result\";\nimport { databaseReadDefaults, databaseWriteDefaults } from \"./defaults\";\n\n/** Apply AppKit execution policy to an entity operation. */\nexport type EntityExecute = <T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: { default: PluginExecuteConfig; user?: PluginExecuteConfig },\n) => Promise<ExecutionResult<T>>;\n\n/** Runtime dependencies shared by immutable clients for one table. */\nexport interface EntityClientContext {\n readonly table: AppKitTable;\n readonly getDataPath: () => DataPath;\n readonly execute: EntityExecute;\n readonly assertActive: () => void;\n}\n\n/** Clone plain acyclic input so caller mutation cannot change a built query. */\nfunction snapshotPlain<T>(value: T, seen = new Set<object>()): T {\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"boolean\" ||\n typeof value === \"bigint\" ||\n (typeof value === \"number\" && Number.isFinite(value))\n ) {\n return value;\n }\n if (typeof value !== \"object\") {\n throw new DatabasePluginError(\"INVALID_REQUEST\", \"read\");\n }\n if (seen.has(value)) throw new DatabasePluginError(\"INVALID_REQUEST\", \"read\");\n if (\n !Array.isArray(value) &&\n Object.getPrototypeOf(value) !== Object.prototype\n )\n throw new DatabasePluginError(\"INVALID_REQUEST\", \"read\");\n seen.add(value);\n const copy = (\n Array.isArray(value)\n ? value.map((item) => snapshotPlain(item, seen))\n : Object.fromEntries(\n Object.entries(value).map(([key, item]) => [\n key,\n snapshotPlain(item, seen),\n ]),\n )\n ) as T;\n seen.delete(value);\n return copy;\n}\n\n/** Report eager fluent-input failures through the public read phase. */\nfunction validateReadInput<T>(validate: () => T): T {\n try {\n return validate();\n } catch (error) {\n throw classifyDatabaseError(error, \"read\");\n }\n}\n\n/**\n * Plugin-facing client that composes immutable query state. The DataPath remains\n * the schema-aware validation and execution boundary.\n */\nexport class EntityClient {\n constructor(\n private readonly ctx: EntityClientContext,\n private readonly state: QuerySpec = {},\n ) {}\n\n private clone(state: QuerySpec): EntityClient {\n return new EntityClient(this.ctx, state);\n }\n\n /** Snapshot and compose predicates; the adapter validates columns/operators. */\n where(filter: WhereClause): EntityClient {\n const snapshot = snapshotPlain(filter);\n return this.clone({\n ...this.state,\n where: andWhere(this.state.where, snapshot),\n });\n }\n\n /** Merge ordering, replacing the direction for repeated columns. */\n order(order: OrderSpec): EntityClient {\n const snapshot = snapshotPlain(order);\n return this.clone({\n ...this.state,\n order: { ...this.state.order, ...snapshot },\n });\n }\n\n select(columns: readonly string[]): EntityClient {\n return this.clone({ ...this.state, select: snapshotPlain(columns) });\n }\n\n /** Merge relation includes, replacing repeated relation options. */\n include(include: IncludeSpec): EntityClient {\n const snapshot = snapshotPlain(include);\n return this.clone({\n ...this.state,\n include: { ...this.state.include, ...snapshot },\n });\n }\n\n limit(limit: number): EntityClient {\n return validateReadInput(() =>\n this.clone({ ...this.state, limit: validateLimit(limit) }),\n );\n }\n\n offset(offset: number): EntityClient {\n return validateReadInput(() =>\n this.clone({ ...this.state, offset: validateOffset(offset) }),\n );\n }\n\n /** Execute a collection read; DataPath applies the default upper bound. */\n async toArray(): Promise<Row[]> {\n return this.run(\"read\", (dataPath) =>\n dataPath.select(this.ctx.table, this.state),\n );\n }\n\n async first(): Promise<Row | null> {\n return (await this.limit(1).toArray())[0] ?? null;\n }\n\n find(id: IdValue): Promise<Row | null> {\n return this.run(\"read\", (dataPath) =>\n dataPath.findOne(this.ctx.table, id, {\n where: this.state.where,\n select: this.state.select,\n include: this.state.include,\n }),\n );\n }\n\n count(): Promise<number> {\n return this.run(\"read\", (dataPath) =>\n dataPath.count(this.ctx.table, this.state.where),\n );\n }\n\n create(values: Row): Promise<Row> {\n return this.write(\"create\", values, (dataPath, payload) =>\n dataPath.insert(this.ctx.table, payload),\n );\n }\n\n update(id: IdValue, values: Row): Promise<Row | null> {\n return this.write(\"update\", values, (dataPath, payload) =>\n dataPath.update(this.ctx.table, id, payload),\n );\n }\n\n upsert(values: Row, options: { onConflict: string }): Promise<Row> {\n const onConflict = options.onConflict;\n return this.write(\"create\", values, (dataPath, payload) =>\n dataPath.upsert(this.ctx.table, payload, onConflict),\n );\n }\n\n delete(id: IdValue): Promise<boolean> {\n return this.run(\n \"write\",\n (dataPath) => dataPath.delete(this.ctx.table, id),\n databaseWriteDefaults,\n );\n }\n\n /** Validate caller values against the finalized trusted-write schema. */\n private async write<T>(\n kind: \"create\" | \"update\",\n values: Row,\n operation: (dataPath: DataPath, values: Row) => Promise<T>,\n ): Promise<T> {\n if (kind === \"update\" && Object.keys(values).length === 0) {\n throw new DatabasePluginError(\"INVALID_REQUEST\", \"write\");\n }\n let parsed: Row;\n try {\n const validator = (\n kind === \"create\"\n ? this.ctx.table.$insertSchema\n : this.ctx.table.$updateSchema\n ) as { parse(value: unknown): Row };\n parsed = validator.parse(values);\n } catch {\n throw new DatabasePluginError(\"INVALID_REQUEST\", \"write\");\n }\n return this.run(\n \"write\",\n (dataPath) => operation(dataPath, parsed),\n databaseWriteDefaults,\n );\n }\n\n /** Execute through AppKit interceptors and expose only safe failures. */\n private async run<T>(\n phase: \"read\" | \"write\",\n operation: (dataPath: DataPath) => Promise<T>,\n defaults: PluginExecuteConfig = databaseReadDefaults,\n ): Promise<T> {\n try {\n this.ctx.assertActive();\n } catch (error) {\n throw classifyDatabaseError(error, phase);\n }\n const result = await this.ctx.execute(\n async () => {\n try {\n this.ctx.assertActive();\n return await operation(this.ctx.getDataPath());\n } catch (error) {\n throw classifyDatabaseError(error, phase);\n }\n },\n { default: defaults },\n );\n if (result.ok) return result.data;\n throw databaseErrorFromStatus(result.status, phase);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"entity-client.js","names":[],"sources":["../../../src/plugins/database/entity-client.ts"],"sourcesContent":["import type { PluginExecuteConfig } from \"shared\";\n\nimport {\n classifyDatabaseError,\n DatabasePluginError,\n databaseErrorFromStatus,\n invalidDatabaseRequest,\n} from \"../../database/errors\";\nimport type {\n DataPath,\n IdValue,\n IncludeSpec,\n OrderSpec,\n QuerySpec,\n Row,\n WhereClause,\n} from \"../../database/runtime\";\nimport {\n andWhere,\n validateLimit,\n validateOffset,\n} from \"../../database/runtime/data-path\";\nimport type { AppKitTable } from \"../../database/schema-builder\";\nimport { DatabaseValidationError } from \"../../errors\";\nimport type { ExecutionResult } from \"../../plugin/execution-result\";\nimport { databaseReadDefaults, databaseWriteDefaults } from \"./defaults\";\nimport type { HookContext, MutationOperation } from \"./hooks\";\nimport type { MutationScope } from \"./scope\";\nimport type { EntityHooks } from \"./types\";\n\n/** Apply AppKit execution policy to an entity operation. */\nexport type EntityExecute = <T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: { default: PluginExecuteConfig; user?: PluginExecuteConfig },\n) => Promise<ExecutionResult<T>>;\n\n/** Runtime dependencies shared by immutable clients for one table. */\nexport interface EntityClientContext {\n readonly table: AppKitTable;\n readonly getDataPath: () => DataPath;\n readonly execute: EntityExecute;\n readonly assertActive: () => void;\n readonly scope: MutationScope;\n readonly hooks?: EntityHooks;\n /** True once this client is bound to an already open transaction. */\n readonly transactionBound: boolean;\n /** Continue on the same table's client inside a new or reused transaction. */\n readonly runInTransaction: <T>(\n run: (entity: EntityClient) => Promise<T>,\n ) => Promise<T>;\n}\n\n/** Internal rollback signal for a hooked keyed mutation that matched no row. */\nclass MutationNoMatchError extends DatabasePluginError {\n constructor() {\n super(\"NOT_FOUND\", \"write\");\n }\n}\n\n/** Clone plain acyclic input so caller mutation cannot change a built query. */\nfunction snapshotPlain<T>(value: T, seen = new Set<object>()): T {\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"boolean\" ||\n typeof value === \"bigint\" ||\n (typeof value === \"number\" && Number.isFinite(value))\n ) {\n return value;\n }\n if (typeof value !== \"object\") {\n throw new DatabasePluginError(\"INVALID_REQUEST\", \"read\");\n }\n if (seen.has(value)) throw new DatabasePluginError(\"INVALID_REQUEST\", \"read\");\n if (\n !Array.isArray(value) &&\n Object.getPrototypeOf(value) !== Object.prototype\n )\n throw new DatabasePluginError(\"INVALID_REQUEST\", \"read\");\n seen.add(value);\n const copy = (\n Array.isArray(value)\n ? value.map((item) => snapshotPlain(item, seen))\n : Object.fromEntries(\n Object.entries(value).map(([key, item]) => [\n key,\n snapshotPlain(item, seen),\n ]),\n )\n ) as T;\n seen.delete(value);\n return copy;\n}\n\n/** Report eager fluent-input failures through the public read phase. */\nfunction validateReadInput<T>(validate: () => T): T {\n try {\n return validate();\n } catch (error) {\n throw classifyDatabaseError(error, \"read\");\n }\n}\n\n/** A hook replaced the payload only if it returned an object to persist. */\nfunction isReplacement(value: unknown): value is Row {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Plugin-facing client that composes immutable query state. The DataPath remains\n * the schema-aware validation and execution boundary.\n */\nexport class EntityClient {\n constructor(\n private readonly ctx: EntityClientContext,\n private readonly state: QuerySpec = {},\n ) {}\n\n private clone(state: QuerySpec): EntityClient {\n return new EntityClient(this.ctx, state);\n }\n\n /** Snapshot and compose predicates; the adapter validates columns/operators. */\n where(filter: WhereClause): EntityClient {\n const snapshot = snapshotPlain(filter);\n return this.clone({\n ...this.state,\n where: andWhere(this.state.where, snapshot),\n });\n }\n\n /** Merge ordering, replacing the direction for repeated columns. */\n order(order: OrderSpec): EntityClient {\n const snapshot = snapshotPlain(order);\n return this.clone({\n ...this.state,\n order: { ...this.state.order, ...snapshot },\n });\n }\n\n select(columns: readonly string[]): EntityClient {\n return this.clone({ ...this.state, select: snapshotPlain(columns) });\n }\n\n /** Merge relation includes, replacing repeated relation options. */\n include(include: IncludeSpec): EntityClient {\n const snapshot = snapshotPlain(include);\n return this.clone({\n ...this.state,\n include: { ...this.state.include, ...snapshot },\n });\n }\n\n limit(limit: number): EntityClient {\n return validateReadInput(() =>\n this.clone({ ...this.state, limit: validateLimit(limit) }),\n );\n }\n\n offset(offset: number): EntityClient {\n return validateReadInput(() =>\n this.clone({ ...this.state, offset: validateOffset(offset) }),\n );\n }\n\n /** Execute a collection read; DataPath applies the default upper bound. */\n async toArray(): Promise<Row[]> {\n return this.run(\"read\", (dataPath) =>\n dataPath.select(this.ctx.table, this.state),\n );\n }\n\n async first(): Promise<Row | null> {\n return (await this.limit(1).toArray())[0] ?? null;\n }\n\n find(id: IdValue): Promise<Row | null> {\n return this.run(\"read\", (dataPath) =>\n dataPath.findOne(this.ctx.table, id, {\n where: this.state.where,\n select: this.state.select,\n include: this.state.include,\n }),\n );\n }\n\n count(): Promise<number> {\n return this.run(\"read\", (dataPath) =>\n dataPath.count(this.ctx.table, this.state.where),\n );\n }\n\n async create(values: Row): Promise<Row> {\n this.assertUnfiltered(\"create\");\n const hooks = this.ctx.hooks;\n const insert = (payload: Row, byHook = false) =>\n this.write(\n \"create\",\n payload,\n (dataPath, parsed) => dataPath.insert(this.ctx.table, parsed),\n byHook,\n );\n if (this.isDirect(\"create\")) {\n return this.directMutation(\"create\", () => insert(values));\n }\n return this.mutate(\n \"create\",\n (entity) => entity.create(values),\n async (context) => {\n const payload = await this.runBefore(\n () => hooks?.beforeCreate?.(values, context),\n values,\n );\n const row = await insert(payload, payload !== values);\n await this.runHook(() => hooks?.afterCreate?.(row, context));\n return row;\n },\n );\n }\n\n update(id: IdValue, values: Row): Promise<Row | null> {\n return this.updateWithNoMatch(id, values, new MutationNoMatchError());\n }\n\n private async updateWithNoMatch(\n id: IdValue,\n values: Row,\n noMatch: MutationNoMatchError,\n ): Promise<Row | null> {\n const hooks = this.ctx.hooks;\n const apply = (payload: Row, byHook = false) =>\n this.write(\n \"update\",\n payload,\n (dataPath, parsed) =>\n dataPath.update(this.ctx.table, id, parsed, this.state.where),\n byHook,\n );\n if (this.isDirect(\"update\")) {\n return this.directMutation(\"update\", () => apply(values));\n }\n try {\n return await this.mutate(\n \"update\",\n (entity) => entity.updateWithNoMatch(id, values, noMatch),\n async (context) => {\n const payload = await this.runBefore(\n () => hooks?.beforeUpdate?.(id, values, context),\n values,\n );\n const row = await apply(payload, payload !== values);\n if (row === null) throw noMatch;\n await this.runHook(() => hooks?.afterUpdate?.(row, context));\n return row;\n },\n );\n } catch (error) {\n if (\n error === noMatch &&\n !this.ctx.transactionBound &&\n !this.ctx.scope.activeTransaction()\n ) {\n return null;\n }\n throw error;\n }\n }\n\n async upsert(values: Row, options: { onConflict: string }): Promise<Row> {\n this.assertUnfiltered(\"upsert\");\n const hooks = this.ctx.hooks;\n const onConflict = options.onConflict;\n const apply = (payload: Row, byHook = false) =>\n this.write(\n \"create\",\n payload,\n (dataPath, parsed) =>\n dataPath.upsert(this.ctx.table, parsed, onConflict),\n byHook,\n );\n if (this.isDirect(\"upsert\")) {\n return this.directMutation(\"upsert\", () => apply(values));\n }\n return this.mutate(\n \"upsert\",\n (entity) => entity.upsert(values, { onConflict }),\n async (context) => {\n const payload = await this.runBefore(\n () => hooks?.beforeUpsert?.(values, context),\n values,\n );\n // Which branch the database took stays invisible to the hook.\n const row = await apply(payload, payload !== values);\n await this.runHook(() => hooks?.afterUpsert?.(row, context));\n return row;\n },\n );\n }\n\n delete(id: IdValue): Promise<boolean> {\n return this.deleteWithNoMatch(id, new MutationNoMatchError());\n }\n\n private async deleteWithNoMatch(\n id: IdValue,\n noMatch: MutationNoMatchError,\n ): Promise<boolean> {\n const hooks = this.ctx.hooks;\n const remove = () =>\n this.run(\n \"write\",\n (dataPath) => dataPath.delete(this.ctx.table, id, this.state.where),\n databaseWriteDefaults,\n );\n if (this.isDirect(\"delete\")) {\n return this.directMutation(\"delete\", remove);\n }\n try {\n return await this.mutate(\n \"delete\",\n (entity) => entity.deleteWithNoMatch(id, noMatch),\n async (context) => {\n await this.runHook(() => hooks?.beforeDelete?.(id, context));\n const deleted = await remove();\n if (!deleted) throw noMatch;\n await this.runHook(() => hooks?.afterDelete?.(id, context));\n return true;\n },\n );\n } catch (error) {\n if (\n error === noMatch &&\n !this.ctx.transactionBound &&\n !this.ctx.scope.activeTransaction()\n ) {\n return false;\n }\n throw error;\n }\n }\n\n /** Count every write that runs on an already bound transaction. */\n private directMutation<T>(\n operation: MutationOperation,\n run: () => Promise<T>,\n ): Promise<T> {\n if (!this.ctx.transactionBound) return run();\n return this.ctx.scope.runMutation(this.ctx.table.$name, operation, run);\n }\n\n /** An insert matches no existing row, so a predicate could only be dropped. */\n private assertUnfiltered(operation: \"create\" | \"upsert\"): void {\n if (this.state.where !== undefined) {\n throw invalidDatabaseRequest(\n `${operation}() cannot follow where(); it does not select rows to change`,\n );\n }\n }\n\n /**\n * Go straight to the DataPath only when there is nothing to sequence and no\n * open transaction to join. A root client that skipped the second check would\n * commit on the pool while a transaction it cannot see is still open.\n */\n private isDirect(operation: MutationOperation): boolean {\n if (this.hasHooks(operation)) return false;\n return this.ctx.transactionBound || !this.ctx.scope.activeTransaction();\n }\n\n private hasHooks(operation: MutationOperation): boolean {\n const hooks = this.ctx.hooks;\n if (!hooks) return false;\n switch (operation) {\n case \"create\":\n return Boolean(hooks.beforeCreate ?? hooks.afterCreate);\n case \"update\":\n return Boolean(hooks.beforeUpdate ?? hooks.afterUpdate);\n case \"upsert\":\n return Boolean(hooks.beforeUpsert ?? hooks.afterUpsert);\n case \"delete\":\n return Boolean(hooks.beforeDelete ?? hooks.afterDelete);\n }\n }\n\n /**\n * Run one mutation atomically. A top-level call restarts itself on the\n * transaction-bound client, so hooks, validation, the mutation, and its after\n * hook commit or roll back together. An unhooked root mutation reaches this\n * only to join an open transaction; its bound client counts the direct write\n * without constructing a hook context.\n */\n private async mutate<T>(\n operation: MutationOperation,\n restart: (entity: EntityClient) => Promise<T>,\n body: (context: HookContext) => Promise<T>,\n ): Promise<T> {\n if (this.ctx.transactionBound) {\n return this.ctx.scope.runMutation(this.ctx.table.$name, operation, () =>\n body(this.hookContext()),\n );\n }\n try {\n return await this.ctx.runInTransaction((entity) =>\n // Change the execution context without losing the caller's query state\n // or mutating the shared transaction client.\n restart(entity.clone(this.state)),\n );\n } catch (error) {\n if (\n error instanceof DatabaseValidationError ||\n error instanceof MutationNoMatchError\n ) {\n throw error;\n }\n // BEGIN and COMMIT fail outside the inner classification boundary.\n throw classifyDatabaseError(error, \"write\");\n }\n }\n\n /** Resolve the surface at hook time so it cannot outlive its transaction. */\n private hookContext(): HookContext {\n const database = this.ctx.scope.activeTransaction();\n if (!database) throw new DatabasePluginError(\"INTERNAL\", \"write\");\n return { entity: this.ctx.table.$name, app: { database } };\n }\n\n /** Substitute the payload a before hook returned, if it returned one. */\n private async runBefore(invoke: () => unknown, values: Row): Promise<Row> {\n const replacement = await this.runHook(invoke);\n if (replacement === undefined || replacement === null) return values;\n if (!isReplacement(replacement)) {\n throw new DatabasePluginError(\"INTERNAL\", \"write\");\n }\n return replacement;\n }\n\n /** Keep a deliberate validation signal; collapse every other hook failure. */\n private async runHook(invoke: () => unknown): Promise<unknown> {\n try {\n return await invoke();\n } catch (error) {\n if (error instanceof DatabaseValidationError) throw error;\n throw classifyDatabaseError(error, \"write\");\n }\n }\n\n /** Validate caller values against the finalized trusted-write schema. */\n private async write<T>(\n kind: \"create\" | \"update\",\n values: Row,\n operation: (dataPath: DataPath, values: Row) => Promise<T>,\n // A payload a hook replaced is server-authored, so blaming the caller lies.\n byHook = false,\n ): Promise<T> {\n const rejection = byHook ? \"INTERNAL\" : \"INVALID_REQUEST\";\n if (kind === \"update\" && Object.keys(values).length === 0) {\n throw new DatabasePluginError(rejection, \"write\");\n }\n let parsed: Row;\n try {\n const validator = (\n kind === \"create\"\n ? this.ctx.table.$insertSchema\n : this.ctx.table.$updateSchema\n ) as { parse(value: unknown): Row };\n parsed = validator.parse(values);\n } catch {\n throw new DatabasePluginError(rejection, \"write\");\n }\n return this.run(\n \"write\",\n (dataPath) => operation(dataPath, parsed),\n databaseWriteDefaults,\n );\n }\n\n /** Execute through AppKit interceptors and expose only safe failures. */\n private async run<T>(\n phase: \"read\" | \"write\",\n operation: (dataPath: DataPath) => Promise<T>,\n defaults: PluginExecuteConfig = databaseReadDefaults,\n ): Promise<T> {\n try {\n this.ctx.assertActive();\n } catch (error) {\n throw classifyDatabaseError(error, phase);\n }\n const result = await this.ctx.execute(\n async () => {\n try {\n this.ctx.assertActive();\n return await operation(this.ctx.getDataPath());\n } catch (error) {\n throw classifyDatabaseError(error, phase);\n }\n },\n { default: defaults },\n );\n if (result.ok) return result.data;\n throw databaseErrorFromStatus(result.status, phase);\n }\n}\n"],"mappings":"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"}
|
|
@@ -81,5 +81,5 @@ type DatabaseExports = TransactionClient & {
|
|
|
81
81
|
transaction<T>(callback: (tx: TransactionClient) => Promise<T>): Promise<T>;
|
|
82
82
|
};
|
|
83
83
|
//#endregion
|
|
84
|
-
export { DatabaseExports };
|
|
84
|
+
export { DatabaseExports, TransactionClient };
|
|
85
85
|
//# sourceMappingURL=entity-types.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DatabaseRegistry, DatabaseRegistryEntry } from "../../database/contract/registry.js";
|
|
2
|
+
import { IdValue } from "../../database/contract/wire.js";
|
|
3
|
+
import { TransactionClient } from "./entity-types.js";
|
|
4
|
+
import { Row } from "../../database/runtime/data-path.js";
|
|
5
|
+
|
|
6
|
+
//#region src/plugins/database/hooks.d.ts
|
|
7
|
+
/** The only capability a hook receives: entities bound to its transaction. */
|
|
8
|
+
interface HookApp {
|
|
9
|
+
readonly database: TransactionClient;
|
|
10
|
+
}
|
|
11
|
+
/** Which entity is being mutated, and the surface a hook may write through. */
|
|
12
|
+
interface HookContext {
|
|
13
|
+
readonly entity: string;
|
|
14
|
+
readonly app: HookApp;
|
|
15
|
+
}
|
|
16
|
+
type FacetOf<TTable, TFacet extends "row" | "insert" | "update"> = TTable extends keyof DatabaseRegistry ? DatabaseRegistry[TTable] extends DatabaseRegistryEntry ? DatabaseRegistry[TTable][TFacet] : Row : Row;
|
|
17
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
18
|
+
/**
|
|
19
|
+
* Mutation lifecycle for one entity. A before hook may return a replacement
|
|
20
|
+
* payload, which is revalidated against the trusted schema before it is
|
|
21
|
+
* persisted. Every hook, the mutation, and any write a hook issues through
|
|
22
|
+
* `ctx.app.database` share one transaction, so a rejection anywhere rolls all
|
|
23
|
+
* of them back. Throw `DatabaseValidationError` to answer a generated route
|
|
24
|
+
* with `422`; any other failure stays an opaque server error.
|
|
25
|
+
*/
|
|
26
|
+
interface EntityMutationHooks<TTable extends string = string> {
|
|
27
|
+
beforeCreate?(values: FacetOf<TTable, "insert">, context: HookContext): MaybePromise<FacetOf<TTable, "insert"> | void>;
|
|
28
|
+
afterCreate?(row: FacetOf<TTable, "row">, context: HookContext): MaybePromise<void>;
|
|
29
|
+
beforeUpdate?(id: IdValue, values: FacetOf<TTable, "update">, context: HookContext): MaybePromise<FacetOf<TTable, "update"> | void>;
|
|
30
|
+
afterUpdate?(row: FacetOf<TTable, "row">, context: HookContext): MaybePromise<void>;
|
|
31
|
+
beforeUpsert?(values: FacetOf<TTable, "insert">, context: HookContext): MaybePromise<FacetOf<TTable, "insert"> | void>;
|
|
32
|
+
afterUpsert?(row: FacetOf<TTable, "row">, context: HookContext): MaybePromise<void>;
|
|
33
|
+
beforeDelete?(id: IdValue, context: HookContext): MaybePromise<void>;
|
|
34
|
+
afterDelete?(id: IdValue, context: HookContext): MaybePromise<void>;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { EntityMutationHooks, HookApp, HookContext };
|
|
38
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","names":[],"sources":["../../../src/plugins/database/hooks.ts"],"mappings":";;;;;;;UAWiB,OAAA;EAAA,SACN,QAAA,EAAU,iBAAA;AAAA;;UAIJ,WAAA;EAAA,SACN,MAAA;EAAA,SACA,GAAA,EAAK,OAAA;AAAA;AAAA,KAIX,OAAA,uDAGD,MAAA,eAAqB,gBAAA,GACrB,gBAAA,CAAiB,MAAA,UAAgB,qBAAA,GAC/B,gBAAA,CAAiB,MAAA,EAAQ,MAAA,IACzB,GAAA,GACF,GAAA;AAAA,KAEC,YAAA,MAAkB,CAAA,GAAI,OAAA,CAAQ,CAAA;;;;;;;AAZlC;;UAsBgB,mBAAA;EACf,YAAA,EACE,MAAA,EAAQ,OAAA,CAAQ,MAAA,aAChB,OAAA,EAAS,WAAA,GACR,YAAA,CAAa,OAAA,CAAQ,MAAA;EACxB,WAAA,EACE,GAAA,EAAK,OAAA,CAAQ,MAAA,UACb,OAAA,EAAS,WAAA,GACR,YAAA;EACH,YAAA,EACE,EAAA,EAAI,OAAA,EACJ,MAAA,EAAQ,OAAA,CAAQ,MAAA,aAChB,OAAA,EAAS,WAAA,GACR,YAAA,CAAa,OAAA,CAAQ,MAAA;EACxB,WAAA,EACE,GAAA,EAAK,OAAA,CAAQ,MAAA,UACb,OAAA,EAAS,WAAA,GACR,YAAA;EACH,YAAA,EACE,MAAA,EAAQ,OAAA,CAAQ,MAAA,aAChB,OAAA,EAAS,WAAA,GACR,YAAA,CAAa,OAAA,CAAQ,MAAA;EACxB,WAAA,EACE,GAAA,EAAK,OAAA,CAAQ,MAAA,UACb,OAAA,EAAS,WAAA,GACR,YAAA;EACH,YAAA,EAAc,EAAA,EAAI,OAAA,EAAS,OAAA,EAAS,WAAA,GAAc,YAAA;EAClD,WAAA,EAAa,EAAA,EAAI,OAAA,EAAS,OAAA,EAAS,WAAA,GAAc,YAAA;AAAA"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import { DatabaseExports } from "./entity-types.js";
|
|
2
|
-
import {
|
|
1
|
+
import { DatabaseExports, TransactionClient } from "./entity-types.js";
|
|
2
|
+
import { EntityMutationHooks, HookApp, HookContext } from "./hooks.js";
|
|
3
|
+
import { DatabaseApiConfig, DatabaseApiWriteOperation, DatabaseApiWritesConfig, EntityHooks, IDatabaseConfig, ReadSerializer, ReadSerializerContext } from "./types.js";
|
|
3
4
|
import { database } from "./database.js";
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { createLogger } from "../../logging/logger.js";
|
|
2
|
+
import { DatabaseValidationError } from "../../errors/database-validation.js";
|
|
3
|
+
import "../../errors/index.js";
|
|
2
4
|
import { createLakebasePool } from "../../connectors/lakebase/index.js";
|
|
3
5
|
import { assertFinalizedSchema } from "../../database/schema-builder/define-schema.js";
|
|
4
6
|
import { DatabasePluginError, classifyDatabaseError } from "../../database/errors.js";
|
|
5
|
-
import { STATEMENT_TIMEOUT_MS } from "./defaults.js";
|
|
7
|
+
import { IDLE_IN_TRANSACTION_TIMEOUT_MS, STATEMENT_TIMEOUT_MS, TRANSACTION_TIMEOUT_MS } from "./defaults.js";
|
|
6
8
|
import { createDrizzleDataPath, createDrizzleDb } from "../../database/runtime/engine/drizzle-data-path.js";
|
|
7
9
|
import { EntityClient } from "./entity-client.js";
|
|
10
|
+
import { createMutationScope } from "./scope.js";
|
|
8
11
|
|
|
9
12
|
//#region src/plugins/database/lifecycle.ts
|
|
10
13
|
const logger = createLogger("database");
|
|
@@ -13,6 +16,7 @@ function buildSql(context) {
|
|
|
13
16
|
return async (strings, ...values) => {
|
|
14
17
|
try {
|
|
15
18
|
context.assertActive();
|
|
19
|
+
if (context.transactionBound) context.scope.consumeTransactionOperation();
|
|
16
20
|
return await context.getDataPath().raw(strings, ...values);
|
|
17
21
|
} catch (error) {
|
|
18
22
|
throw classifyDatabaseError(error, "read");
|
|
@@ -26,47 +30,76 @@ function buildTransactionClient(context) {
|
|
|
26
30
|
table,
|
|
27
31
|
getDataPath: context.getDataPath,
|
|
28
32
|
execute: context.execute,
|
|
29
|
-
assertActive: context.assertActive
|
|
33
|
+
assertActive: context.assertActive,
|
|
34
|
+
scope: context.scope,
|
|
35
|
+
hooks: context.hooks?.[name],
|
|
36
|
+
transactionBound: context.transactionBound,
|
|
37
|
+
runInTransaction: (run) => runTransaction(context, (tx) => run(entityOf(tx, name)))
|
|
30
38
|
});
|
|
31
39
|
result.sql = buildSql(context);
|
|
32
40
|
return result;
|
|
33
41
|
}
|
|
42
|
+
/** Typegen owns the entity keys, so reaching one by table name stays dynamic. */
|
|
43
|
+
function entityOf(tx, name) {
|
|
44
|
+
return tx[name];
|
|
45
|
+
}
|
|
46
|
+
/** Reject from inside the driver callback so the driver rolls back on expiry. */
|
|
47
|
+
function runWithTransactionDeadline(run) {
|
|
48
|
+
let timer;
|
|
49
|
+
const deadline = new Promise((_, reject) => {
|
|
50
|
+
timer = setTimeout(() => reject(new DatabasePluginError("TRANSIENT", "transaction")), TRANSACTION_TIMEOUT_MS);
|
|
51
|
+
timer.unref?.();
|
|
52
|
+
});
|
|
53
|
+
return Promise.race([Promise.resolve().then(run), deadline]).finally(() => clearTimeout(timer));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Run inside one transaction, joining the active one rather than nesting:
|
|
57
|
+
* PostgreSQL has no nested transaction, and a savepoint would let part of a
|
|
58
|
+
* hooked mutation survive a rollback.
|
|
59
|
+
*/
|
|
60
|
+
function runTransaction(context, callback) {
|
|
61
|
+
context.assertActive();
|
|
62
|
+
const active = context.scope.activeTransaction();
|
|
63
|
+
if (active) return callback(active);
|
|
64
|
+
return context.getDataPath().transaction(async (txDataPath) => {
|
|
65
|
+
let open = true;
|
|
66
|
+
const assertTransactionActive = () => {
|
|
67
|
+
context.assertActive();
|
|
68
|
+
if (!open) throw new DatabasePluginError("INTERNAL", "transaction");
|
|
69
|
+
};
|
|
70
|
+
const directExecute = async (operation) => ({
|
|
71
|
+
ok: true,
|
|
72
|
+
data: await operation()
|
|
73
|
+
});
|
|
74
|
+
const tx = buildTransactionClient({
|
|
75
|
+
...context,
|
|
76
|
+
getDataPath: () => txDataPath,
|
|
77
|
+
execute: directExecute,
|
|
78
|
+
assertActive: assertTransactionActive,
|
|
79
|
+
transactionBound: true
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
return await runWithTransactionDeadline(() => context.scope.runWithTransaction(tx, () => callback(tx)));
|
|
83
|
+
} finally {
|
|
84
|
+
open = false;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
34
88
|
/** Add the transaction entry point to the root database surface. */
|
|
35
89
|
function buildDatabaseExports(context) {
|
|
36
90
|
const result = buildTransactionClient(context);
|
|
37
91
|
result.transaction = async (callback) => {
|
|
38
92
|
try {
|
|
39
|
-
context
|
|
40
|
-
return await context.getDataPath().transaction(async (txDataPath) => {
|
|
41
|
-
let active = true;
|
|
42
|
-
const assertTransactionActive = () => {
|
|
43
|
-
context.assertActive();
|
|
44
|
-
if (!active) throw new DatabasePluginError("INTERNAL", "transaction");
|
|
45
|
-
};
|
|
46
|
-
const directExecute = async (operation) => ({
|
|
47
|
-
ok: true,
|
|
48
|
-
data: await operation()
|
|
49
|
-
});
|
|
50
|
-
const tx = buildTransactionClient({
|
|
51
|
-
...context,
|
|
52
|
-
getDataPath: () => txDataPath,
|
|
53
|
-
execute: directExecute,
|
|
54
|
-
assertActive: assertTransactionActive
|
|
55
|
-
});
|
|
56
|
-
try {
|
|
57
|
-
return await callback(tx);
|
|
58
|
-
} finally {
|
|
59
|
-
active = false;
|
|
60
|
-
}
|
|
61
|
-
});
|
|
93
|
+
return await runTransaction(context, callback);
|
|
62
94
|
} catch (error) {
|
|
95
|
+
if (error instanceof DatabaseValidationError) throw error;
|
|
63
96
|
throw classifyDatabaseError(error, "transaction");
|
|
64
97
|
}
|
|
65
98
|
};
|
|
66
99
|
return result;
|
|
67
100
|
}
|
|
68
101
|
/** Validate the schema, create one pool-backed API, and verify connectivity. */
|
|
69
|
-
async function createDatabaseState(schema, execute) {
|
|
102
|
+
async function createDatabaseState(schema, execute, hooks) {
|
|
70
103
|
try {
|
|
71
104
|
assertFinalizedSchema(schema);
|
|
72
105
|
} catch (error) {
|
|
@@ -79,13 +112,19 @@ async function createDatabaseState(schema, execute) {
|
|
|
79
112
|
if (!active) throw new DatabasePluginError("INTERNAL", "runtime");
|
|
80
113
|
};
|
|
81
114
|
try {
|
|
82
|
-
pool = createLakebasePool({
|
|
115
|
+
pool = createLakebasePool({
|
|
116
|
+
statement_timeout: STATEMENT_TIMEOUT_MS,
|
|
117
|
+
idle_in_transaction_session_timeout: IDLE_IN_TRANSACTION_TIMEOUT_MS
|
|
118
|
+
});
|
|
83
119
|
const dataPath = createDrizzleDataPath(createDrizzleDb(pool, schema), schema, { columnAccess: "trusted" });
|
|
84
120
|
const exports = buildDatabaseExports({
|
|
85
121
|
schema,
|
|
86
122
|
getDataPath: () => dataPath,
|
|
87
123
|
execute,
|
|
88
|
-
assertActive
|
|
124
|
+
assertActive,
|
|
125
|
+
scope: createMutationScope(),
|
|
126
|
+
hooks,
|
|
127
|
+
transactionBound: false
|
|
89
128
|
});
|
|
90
129
|
await dataPath.raw`select 1`;
|
|
91
130
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.js","names":[],"sources":["../../../src/plugins/database/lifecycle.ts"],"sourcesContent":["import { createLakebasePool } from \"../../connectors/lakebase\";\nimport {\n classifyDatabaseError,\n DatabasePluginError,\n} from \"../../database/errors\";\nimport type { DataPath } from \"../../database/runtime\";\nimport {\n createDrizzleDataPath,\n createDrizzleDb,\n} from \"../../database/runtime/engine/drizzle-data-path\";\nimport type { Schema } from \"../../database/schema-builder\";\nimport { assertFinalizedSchema } from \"../../database/schema-builder/define-schema\";\nimport { createLogger } from \"../../logging/logger\";\nimport {
|
|
1
|
+
{"version":3,"file":"lifecycle.js","names":[],"sources":["../../../src/plugins/database/lifecycle.ts"],"sourcesContent":["import { createLakebasePool } from \"../../connectors/lakebase\";\nimport {\n classifyDatabaseError,\n DatabasePluginError,\n} from \"../../database/errors\";\nimport type { DataPath } from \"../../database/runtime\";\nimport {\n createDrizzleDataPath,\n createDrizzleDb,\n} from \"../../database/runtime/engine/drizzle-data-path\";\nimport type { Schema } from \"../../database/schema-builder\";\nimport { assertFinalizedSchema } from \"../../database/schema-builder/define-schema\";\nimport { DatabaseValidationError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport {\n IDLE_IN_TRANSACTION_TIMEOUT_MS,\n STATEMENT_TIMEOUT_MS,\n TRANSACTION_TIMEOUT_MS,\n} from \"./defaults\";\nimport { EntityClient, type EntityExecute } from \"./entity-client\";\nimport type {\n DatabaseExports,\n SqlTag,\n TransactionClient,\n} from \"./entity-types\";\nimport { createMutationScope, type MutationScope } from \"./scope\";\nimport type { DatabaseHooks } from \"./types\";\n\nconst logger = createLogger(\"database\");\n\n/** Resources owned by one successfully initialized plugin instance. */\nexport interface DatabaseState {\n readonly pool: ReturnType<typeof createLakebasePool>;\n readonly exports: DatabaseExports;\n readonly deactivate: () => void;\n}\n\ninterface ExportContext {\n readonly schema: Schema;\n readonly getDataPath: () => DataPath;\n readonly execute: EntityExecute;\n readonly assertActive: () => void;\n readonly scope: MutationScope;\n readonly hooks?: DatabaseHooks;\n readonly transactionBound: boolean;\n}\n\n/** Execute value-only SQL on the bound DataPath without per-statement retries. */\nfunction buildSql(context: ExportContext): SqlTag {\n return async <T = Record<string, unknown>>(\n strings: TemplateStringsArray,\n ...values: unknown[]\n ) => {\n try {\n context.assertActive();\n if (context.transactionBound) {\n context.scope.consumeTransactionOperation();\n }\n // DataPath accepts value interpolation only and keeps Drizzle private.\n return await context.getDataPath().raw<T>(strings, ...values);\n } catch (error) {\n throw classifyDatabaseError(error, \"read\");\n }\n };\n}\n\n/** Build entities and SQL bound to either the root pool or one transaction. */\nfunction buildTransactionClient(context: ExportContext): TransactionClient {\n const result: Record<string, unknown> = Object.create(null);\n for (const [name, table] of Object.entries(context.schema.$tables)) {\n result[name] = new EntityClient({\n table,\n getDataPath: context.getDataPath,\n execute: context.execute,\n assertActive: context.assertActive,\n scope: context.scope,\n hooks: context.hooks?.[name],\n transactionBound: context.transactionBound,\n runInTransaction: (run) =>\n runTransaction(context, (tx) => run(entityOf(tx, name))),\n });\n }\n result.sql = buildSql(context);\n return result as TransactionClient;\n}\n\n/** Typegen owns the entity keys, so reaching one by table name stays dynamic. */\nfunction entityOf(tx: TransactionClient, name: string): EntityClient {\n return (tx as unknown as Record<string, EntityClient>)[name];\n}\n\n/** Reject from inside the driver callback so the driver rolls back on expiry. */\nfunction runWithTransactionDeadline<T>(run: () => Promise<T>): Promise<T> {\n let timer: ReturnType<typeof setTimeout>;\n const deadline = new Promise<never>((_, reject) => {\n timer = setTimeout(\n () => reject(new DatabasePluginError(\"TRANSIENT\", \"transaction\")),\n TRANSACTION_TIMEOUT_MS,\n );\n timer.unref?.();\n });\n return Promise.race([Promise.resolve().then(run), deadline]).finally(() =>\n clearTimeout(timer),\n );\n}\n\n/**\n * Run inside one transaction, joining the active one rather than nesting:\n * PostgreSQL has no nested transaction, and a savepoint would let part of a\n * hooked mutation survive a rollback.\n */\nfunction runTransaction<T>(\n context: ExportContext,\n callback: (tx: TransactionClient) => Promise<T>,\n): Promise<T> {\n context.assertActive();\n const active = context.scope.activeTransaction();\n if (active) return callback(active);\n return context.getDataPath().transaction(async (txDataPath) => {\n let open = true;\n const assertTransactionActive = () => {\n context.assertActive();\n if (!open) throw new DatabasePluginError(\"INTERNAL\", \"transaction\");\n };\n // The outer transaction owns execution; inner clients use its connection.\n const directExecute: EntityExecute = async (operation) => ({\n ok: true,\n data: await operation(),\n });\n const tx = buildTransactionClient({\n ...context,\n getDataPath: () => txDataPath,\n execute: directExecute,\n assertActive: assertTransactionActive,\n transactionBound: true,\n });\n try {\n return await runWithTransactionDeadline(() =>\n context.scope.runWithTransaction(tx, () => callback(tx)),\n );\n } finally {\n // Captured clients must not outlive the transaction callback.\n open = false;\n }\n });\n}\n\n/** Add the transaction entry point to the root database surface. */\nfunction buildDatabaseExports(context: ExportContext): DatabaseExports {\n const result = buildTransactionClient(context) as DatabaseExports;\n result.transaction = async <T>(\n callback: (tx: TransactionClient) => Promise<T>,\n ) => {\n try {\n return await runTransaction(context, callback);\n } catch (error) {\n if (error instanceof DatabaseValidationError) throw error;\n throw classifyDatabaseError(error, \"transaction\");\n }\n };\n return result;\n}\n\n/** Validate the schema, create one pool-backed API, and verify connectivity. */\nexport async function createDatabaseState<TSchema extends Schema>(\n schema: TSchema,\n execute: EntityExecute,\n hooks?: DatabaseHooks,\n): Promise<DatabaseState> {\n try {\n assertFinalizedSchema(schema);\n } catch (error) {\n logger.error(\"Database schema failed validation: %O\", error);\n throw new DatabasePluginError(\"SETUP_FAILED\", \"setup\");\n }\n\n let active = true;\n let pool: ReturnType<typeof createLakebasePool> | undefined;\n const assertActive = () => {\n if (!active) throw new DatabasePluginError(\"INTERNAL\", \"runtime\");\n };\n try {\n pool = createLakebasePool({\n statement_timeout: STATEMENT_TIMEOUT_MS,\n idle_in_transaction_session_timeout: IDLE_IN_TRANSACTION_TIMEOUT_MS,\n });\n const db = createDrizzleDb(pool, schema);\n const dataPath = createDrizzleDataPath(db, schema, {\n columnAccess: \"trusted\",\n });\n const exports = buildDatabaseExports({\n schema,\n getDataPath: () => dataPath,\n execute,\n assertActive,\n // One scope per state keeps concurrent instances from sharing a transaction.\n scope: createMutationScope(),\n hooks,\n transactionBound: false,\n });\n // Do not publish exports until an authenticated statement succeeds.\n await dataPath.raw`select 1`;\n return {\n pool,\n exports,\n deactivate: () => {\n active = false;\n },\n };\n } catch (error) {\n logger.error(\"Database setup failed: %O\", error);\n active = false;\n await pool?.end().catch(() => undefined);\n throw new DatabasePluginError(\"SETUP_FAILED\", \"setup\");\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { DatabasePluginError } from "../../database/errors.js";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
|
|
4
|
+
//#region src/plugins/database/scope.ts
|
|
5
|
+
/** How deep hook-issued mutations may nest before the guard rejects. */
|
|
6
|
+
const MAX_MUTATION_DEPTH = 8;
|
|
7
|
+
/** Max database operations admitted by one transaction. */
|
|
8
|
+
const MAX_TRANSACTION_OPERATIONS = 100;
|
|
9
|
+
/** Charge one operation to a transaction-owned shared budget. */
|
|
10
|
+
function consumeBudget(state, phase) {
|
|
11
|
+
const budget = state?.budget ?? { used: 0 };
|
|
12
|
+
if (budget.used >= MAX_TRANSACTION_OPERATIONS) throw new DatabasePluginError("INTERNAL", phase);
|
|
13
|
+
budget.used += 1;
|
|
14
|
+
return budget;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Async context owned by one plugin instance. It carries the transaction,
|
|
18
|
+
* mutation budget, and frames that bound recursion, so neither two instances
|
|
19
|
+
* nor two concurrent calls can observe each other.
|
|
20
|
+
*/
|
|
21
|
+
function createMutationScope() {
|
|
22
|
+
const storage = new AsyncLocalStorage();
|
|
23
|
+
return {
|
|
24
|
+
activeTransaction() {
|
|
25
|
+
return storage.getStore()?.transaction;
|
|
26
|
+
},
|
|
27
|
+
runWithTransaction(transaction, run) {
|
|
28
|
+
const current = storage.getStore();
|
|
29
|
+
return storage.run({
|
|
30
|
+
transaction,
|
|
31
|
+
frames: current?.frames ?? [],
|
|
32
|
+
budget: current?.budget ?? { used: 0 }
|
|
33
|
+
}, run);
|
|
34
|
+
},
|
|
35
|
+
consumeTransactionOperation() {
|
|
36
|
+
const state = storage.getStore();
|
|
37
|
+
if (state?.transaction) consumeBudget(state, "transaction");
|
|
38
|
+
},
|
|
39
|
+
async runMutation(entity, operation, run) {
|
|
40
|
+
const state = storage.getStore();
|
|
41
|
+
const frames = state?.frames ?? [];
|
|
42
|
+
if (frames.some((frame) => frame.entity === entity && frame.operation === operation) || frames.length >= MAX_MUTATION_DEPTH) throw new DatabasePluginError("INTERNAL", "write");
|
|
43
|
+
const budget = consumeBudget(state, "write");
|
|
44
|
+
return storage.run({
|
|
45
|
+
transaction: state?.transaction,
|
|
46
|
+
frames: [...frames, {
|
|
47
|
+
entity,
|
|
48
|
+
operation
|
|
49
|
+
}],
|
|
50
|
+
budget
|
|
51
|
+
}, run);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
export { createMutationScope };
|
|
58
|
+
//# sourceMappingURL=scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope.js","names":[],"sources":["../../../src/plugins/database/scope.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\n\nimport { DatabasePluginError } from \"../../database/errors\";\nimport type { TransactionClient } from \"./entity-types\";\nimport type { MutationOperation } from \"./hooks\";\n\n/** How deep hook-issued mutations may nest before the guard rejects. */\nconst MAX_MUTATION_DEPTH = 8;\n/** Max database operations admitted by one transaction. */\nexport const MAX_TRANSACTION_OPERATIONS = 100;\n\ninterface MutationFrame {\n readonly entity: string;\n readonly operation: MutationOperation;\n}\n\ninterface MutationBudget {\n used: number;\n}\n\ninterface ScopeState {\n readonly transaction?: TransactionClient;\n readonly frames: readonly MutationFrame[];\n readonly budget: MutationBudget;\n}\n\n/** Charge one operation to a transaction-owned shared budget. */\nfunction consumeBudget(\n state: ScopeState | undefined,\n phase: \"transaction\" | \"write\",\n): MutationBudget {\n const budget = state?.budget ?? { used: 0 };\n if (budget.used >= MAX_TRANSACTION_OPERATIONS) {\n throw new DatabasePluginError(\"INTERNAL\", phase);\n }\n budget.used += 1;\n return budget;\n}\n\n/** Instance-owned async context created by `createMutationScope()`. */\nexport type MutationScope = ReturnType<typeof createMutationScope>;\n\n/**\n * Async context owned by one plugin instance. It carries the transaction,\n * mutation budget, and frames that bound recursion, so neither two instances\n * nor two concurrent calls can observe each other.\n */\nexport function createMutationScope() {\n const storage = new AsyncLocalStorage<ScopeState>();\n\n return {\n /** The transaction a hook must join, or `undefined` outside one. */\n activeTransaction(): TransactionClient | undefined {\n return storage.getStore()?.transaction;\n },\n\n /** Publish one transaction to everything the callback awaits. */\n runWithTransaction<T>(\n transaction: TransactionClient,\n run: () => Promise<T>,\n ): Promise<T> {\n const current = storage.getStore();\n return storage.run(\n {\n transaction,\n frames: current?.frames ?? [],\n budget: current?.budget ?? { used: 0 },\n },\n run,\n );\n },\n\n /** Charge SQL and other direct work bound to the active transaction. */\n consumeTransactionOperation(): void {\n const state = storage.getStore();\n if (state?.transaction) consumeBudget(state, \"transaction\");\n },\n\n /** Open one frame, refusing a repeated entity/operation pair or deep nesting. */\n async runMutation<T>(\n entity: string,\n operation: MutationOperation,\n run: () => Promise<T>,\n ): Promise<T> {\n const state = storage.getStore();\n const frames = state?.frames ?? [];\n const repeated = frames.some(\n (frame) => frame.entity === entity && frame.operation === operation,\n );\n if (repeated || frames.length >= MAX_MUTATION_DEPTH) {\n throw new DatabasePluginError(\"INTERNAL\", \"write\");\n }\n const budget = consumeBudget(state, \"write\");\n return storage.run(\n {\n transaction: state?.transaction,\n frames: [...frames, { entity, operation }],\n budget,\n },\n run,\n );\n },\n };\n}\n"],"mappings":";;;;;AAOA,MAAM,qBAAqB;;AAE3B,MAAa,6BAA6B;;AAkB1C,SAAS,cACP,OACA,OACgB;CAChB,MAAM,SAAS,OAAO,UAAU,EAAE,MAAM,GAAG;AAC3C,KAAI,OAAO,QAAQ,2BACjB,OAAM,IAAI,oBAAoB,YAAY,MAAM;AAElD,QAAO,QAAQ;AACf,QAAO;;;;;;;AAWT,SAAgB,sBAAsB;CACpC,MAAM,UAAU,IAAI,mBAA+B;AAEnD,QAAO;EAEL,oBAAmD;AACjD,UAAO,QAAQ,UAAU,EAAE;;EAI7B,mBACE,aACA,KACY;GACZ,MAAM,UAAU,QAAQ,UAAU;AAClC,UAAO,QAAQ,IACb;IACE;IACA,QAAQ,SAAS,UAAU,EAAE;IAC7B,QAAQ,SAAS,UAAU,EAAE,MAAM,GAAG;IACvC,EACD,IACD;;EAIH,8BAAoC;GAClC,MAAM,QAAQ,QAAQ,UAAU;AAChC,OAAI,OAAO,YAAa,eAAc,OAAO,cAAc;;EAI7D,MAAM,YACJ,QACA,WACA,KACY;GACZ,MAAM,QAAQ,QAAQ,UAAU;GAChC,MAAM,SAAS,OAAO,UAAU,EAAE;AAIlC,OAHiB,OAAO,MACrB,UAAU,MAAM,WAAW,UAAU,MAAM,cAAc,UAC3D,IACe,OAAO,UAAU,mBAC/B,OAAM,IAAI,oBAAoB,YAAY,QAAQ;GAEpD,MAAM,SAAS,cAAc,OAAO,QAAQ;AAC5C,UAAO,QAAQ,IACb;IACE,aAAa,OAAO;IACpB,QAAQ,CAAC,GAAG,QAAQ;KAAE;KAAQ;KAAW,CAAC;IAC1C;IACD,EACD,IACD;;EAEJ"}
|