@saacms/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/access/index.d.ts +37 -0
- package/dist/access/index.d.ts.map +1 -0
- package/dist/access/index.js +6 -0
- package/dist/auth/index.d.ts +30 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/codegen/content-migration.d.ts +167 -0
- package/dist/codegen/content-migration.d.ts.map +1 -0
- package/dist/codegen/filter-openapi-for-user.d.ts +100 -0
- package/dist/codegen/filter-openapi-for-user.d.ts.map +1 -0
- package/dist/codegen/index.d.ts +19 -0
- package/dist/codegen/index.d.ts.map +1 -0
- package/dist/codegen/index.js +43 -0
- package/dist/codegen/openapi-types.d.ts +125 -0
- package/dist/codegen/openapi-types.d.ts.map +1 -0
- package/dist/codegen/to-d1-migration.d.ts +88 -0
- package/dist/codegen/to-d1-migration.d.ts.map +1 -0
- package/dist/codegen/to-drizzle.d.ts +131 -0
- package/dist/codegen/to-drizzle.d.ts.map +1 -0
- package/dist/codegen/to-openapi.d.ts +80 -0
- package/dist/codegen/to-openapi.d.ts.map +1 -0
- package/dist/codegen/to-puck-fields.d.ts +109 -0
- package/dist/codegen/to-puck-fields.d.ts.map +1 -0
- package/dist/codegen/to-ts-types.d.ts +59 -0
- package/dist/codegen/to-ts-types.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +94 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +8 -0
- package/dist/host/index.d.ts +109 -0
- package/dist/host/index.d.ts.map +1 -0
- package/dist/host/index.js +16 -0
- package/dist/index-172n82sz.js +4 -0
- package/dist/index-8g8ymd37.js +275 -0
- package/dist/index-a3pnt8yz.js +1494 -0
- package/dist/index-b59hfany.js +3078 -0
- package/dist/index-b7z43xwp.js +6 -0
- package/dist/index-r0at8zaw.js +13 -0
- package/dist/index-zgbq60fy.js +74 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +261 -0
- package/dist/observability/audit.d.ts +65 -0
- package/dist/observability/audit.d.ts.map +1 -0
- package/dist/observability/index.d.ts +2 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/publish/compile.d.ts +76 -0
- package/dist/publish/compile.d.ts.map +1 -0
- package/dist/runtime/auth-middleware.d.ts +34 -0
- package/dist/runtime/auth-middleware.d.ts.map +1 -0
- package/dist/runtime/boolean-columns.d.ts +65 -0
- package/dist/runtime/boolean-columns.d.ts.map +1 -0
- package/dist/runtime/cache.d.ts +62 -0
- package/dist/runtime/cache.d.ts.map +1 -0
- package/dist/runtime/create-route.d.ts +26 -0
- package/dist/runtime/create-route.d.ts.map +1 -0
- package/dist/runtime/delete-route.d.ts +15 -0
- package/dist/runtime/delete-route.d.ts.map +1 -0
- package/dist/runtime/drafts-route.d.ts +65 -0
- package/dist/runtime/drafts-route.d.ts.map +1 -0
- package/dist/runtime/health-route.d.ts +23 -0
- package/dist/runtime/health-route.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +24 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +17 -0
- package/dist/runtime/json-columns.d.ts +50 -0
- package/dist/runtime/json-columns.d.ts.map +1 -0
- package/dist/runtime/list-route.d.ts +20 -0
- package/dist/runtime/list-route.d.ts.map +1 -0
- package/dist/runtime/openapi-route.d.ts +30 -0
- package/dist/runtime/openapi-route.d.ts.map +1 -0
- package/dist/runtime/pattern-route.d.ts +48 -0
- package/dist/runtime/pattern-route.d.ts.map +1 -0
- package/dist/runtime/problem-details.d.ts +32 -0
- package/dist/runtime/problem-details.d.ts.map +1 -0
- package/dist/runtime/put-route.d.ts +19 -0
- package/dist/runtime/put-route.d.ts.map +1 -0
- package/dist/runtime/read-route.d.ts +26 -0
- package/dist/runtime/read-route.d.ts.map +1 -0
- package/dist/runtime/scale-cost.d.ts +84 -0
- package/dist/runtime/scale-cost.d.ts.map +1 -0
- package/dist/runtime/scheme-route.d.ts +49 -0
- package/dist/runtime/scheme-route.d.ts.map +1 -0
- package/dist/runtime/services.d.ts +42 -0
- package/dist/runtime/services.d.ts.map +1 -0
- package/dist/runtime/update-route.d.ts +20 -0
- package/dist/runtime/update-route.d.ts.map +1 -0
- package/dist/runtime/upload-route.d.ts +46 -0
- package/dist/runtime/upload-route.d.ts.map +1 -0
- package/dist/schema/index.d.ts +185 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +40 -0
- package/dist/schema/media.d.ts +237 -0
- package/dist/schema/media.d.ts.map +1 -0
- package/dist/schema/plugin-trust.d.ts +144 -0
- package/dist/schema/plugin-trust.d.ts.map +1 -0
- package/dist/signals/index.d.ts +10 -0
- package/dist/signals/index.d.ts.map +1 -0
- package/dist/signals/index.js +10 -0
- package/dist/storage/index.d.ts +120 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +13 -0
- package/dist/tenant/index.d.ts +105 -0
- package/dist/tenant/index.d.ts.map +1 -0
- package/dist/theme/index.d.ts +56 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/types/ids.d.ts +45 -0
- package/dist/types/ids.d.ts.map +1 -0
- package/dist/types/ids.js +15 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/user.d.ts +14 -0
- package/dist/types/user.d.ts.map +1 -0
- package/dist/types/user.js +1 -0
- package/package.json +116 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/hooks/index.ts
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
var COMMAND_PHASE_MOMENTS = new Set([
|
|
4
|
+
"beforeValidate",
|
|
5
|
+
"beforeChange",
|
|
6
|
+
"beforeRead",
|
|
7
|
+
"beforeDelete",
|
|
8
|
+
"beforePublish",
|
|
9
|
+
"beforeRender"
|
|
10
|
+
]);
|
|
11
|
+
function isCommandPhase(moment) {
|
|
12
|
+
return COMMAND_PHASE_MOMENTS.has(moment);
|
|
13
|
+
}
|
|
14
|
+
function isPlainObject(value) {
|
|
15
|
+
return typeof value === "object" && value !== null;
|
|
16
|
+
}
|
|
17
|
+
function runHooks(moment, ctx, hooks) {
|
|
18
|
+
const command = isCommandPhase(moment);
|
|
19
|
+
if (hooks.length === 0) {
|
|
20
|
+
return command ? Effect.succeed(ctx.data) : Effect.void;
|
|
21
|
+
}
|
|
22
|
+
return command ? runCommandPhase(ctx, hooks) : runEventPhase(moment, ctx, hooks);
|
|
23
|
+
}
|
|
24
|
+
function runCommandPhase(initialCtx, hooks) {
|
|
25
|
+
const seed = Effect.succeed({
|
|
26
|
+
currentData: initialCtx.data,
|
|
27
|
+
lastResult: initialCtx.data
|
|
28
|
+
});
|
|
29
|
+
const chained = hooks.reduce((acc, hook) => Effect.flatMap(acc, (state) => {
|
|
30
|
+
const hookCtx = {
|
|
31
|
+
...initialCtx,
|
|
32
|
+
data: state.currentData
|
|
33
|
+
};
|
|
34
|
+
return Effect.map(hook.fn(hookCtx), (out) => {
|
|
35
|
+
if (isPlainObject(out)) {
|
|
36
|
+
return { currentData: out, lastResult: out };
|
|
37
|
+
}
|
|
38
|
+
return state;
|
|
39
|
+
});
|
|
40
|
+
}), seed);
|
|
41
|
+
return Effect.map(chained, (state) => state.lastResult);
|
|
42
|
+
}
|
|
43
|
+
function runEventPhase(moment, ctx, hooks) {
|
|
44
|
+
const effects = hooks.map((hook) => {
|
|
45
|
+
const effect = hook.fn(ctx);
|
|
46
|
+
if (hook.options?.failOnError === true) {
|
|
47
|
+
return effect;
|
|
48
|
+
}
|
|
49
|
+
return Effect.catchAll(effect, (err) => Effect.sync(() => {
|
|
50
|
+
console.warn(`[saacms/hooks] ${moment} hook failed:`, err);
|
|
51
|
+
}));
|
|
52
|
+
});
|
|
53
|
+
return Effect.all(effects, { concurrency: "unbounded", discard: true });
|
|
54
|
+
}
|
|
55
|
+
function resolveHooksFor(moment, source) {
|
|
56
|
+
const result = [];
|
|
57
|
+
appendEntry(source.collection?.hooks?.[moment], result);
|
|
58
|
+
for (const plugin of source.plugins ?? []) {
|
|
59
|
+
appendEntry(plugin.hooks?.[moment], result);
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
function appendEntry(entry, result) {
|
|
64
|
+
if (typeof entry === "function") {
|
|
65
|
+
result.push({ fn: entry });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (isPlainObject(entry) && typeof entry.fn === "function") {
|
|
69
|
+
const e = entry;
|
|
70
|
+
result.push(e.options !== undefined ? { fn: e.fn, options: e.options } : { fn: e.fn });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { runHooks, resolveHooksFor };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @saacms/core — public surface.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the canonical types and (when implemented) the runtime handler,
|
|
5
|
+
* schema projections, hook orchestrator, signals, and SSE primitives.
|
|
6
|
+
*
|
|
7
|
+
* v1 alpha status: types are stable; implementations are stubs.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./types/index.ts";
|
|
10
|
+
export * from "./schema/index.ts";
|
|
11
|
+
export * from "./theme/index.ts";
|
|
12
|
+
export * from "./access/index.ts";
|
|
13
|
+
export * from "./auth/index.ts";
|
|
14
|
+
export * from "./hooks/index.ts";
|
|
15
|
+
export * from "./runtime/index.ts";
|
|
16
|
+
export * from "./signals/index.ts";
|
|
17
|
+
export * from "./storage/index.ts";
|
|
18
|
+
export * from "./host/index.ts";
|
|
19
|
+
export * from "./codegen/index.ts";
|
|
20
|
+
export * from "./publish/compile.ts";
|
|
21
|
+
export * from "./tenant/index.ts";
|
|
22
|
+
export * from "./observability/index.ts";
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,sBAAsB,CAAA;AACpC,cAAc,mBAAmB,CAAA;AACjC,cAAc,0BAA0B,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import {
|
|
2
|
+
evaluateAccess
|
|
3
|
+
} from "./index-b7z43xwp.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_THRESHOLDS,
|
|
6
|
+
RowStorageError,
|
|
7
|
+
UniqueConstraintError,
|
|
8
|
+
collectServices,
|
|
9
|
+
computeScaleCostSignal,
|
|
10
|
+
createSaacmsRuntime,
|
|
11
|
+
getServices,
|
|
12
|
+
recordAuditEvent
|
|
13
|
+
} from "./index-b59hfany.js";
|
|
14
|
+
import {
|
|
15
|
+
resolveHooksFor,
|
|
16
|
+
runHooks
|
|
17
|
+
} from "./index-zgbq60fy.js";
|
|
18
|
+
import {
|
|
19
|
+
computed,
|
|
20
|
+
effect,
|
|
21
|
+
signal
|
|
22
|
+
} from "./index-172n82sz.js";
|
|
23
|
+
import {
|
|
24
|
+
id
|
|
25
|
+
} from "./index-r0at8zaw.js";
|
|
26
|
+
import {
|
|
27
|
+
assertNoTableNameCollisions,
|
|
28
|
+
camelToSnakeCase,
|
|
29
|
+
collectionToOpenApiPaths,
|
|
30
|
+
contentMigrationModuleSource,
|
|
31
|
+
defaultLabel,
|
|
32
|
+
detectPendingContentMigrations,
|
|
33
|
+
diffBlockSchemas,
|
|
34
|
+
filterOpenApiForUser,
|
|
35
|
+
fingerprintBlockSchema,
|
|
36
|
+
fingerprintToString,
|
|
37
|
+
normalizeDateSchemas,
|
|
38
|
+
schemaToD1Migration,
|
|
39
|
+
schemaToD1Migrations,
|
|
40
|
+
schemaToDrizzle,
|
|
41
|
+
schemaToOpenApiSchema,
|
|
42
|
+
schemaToPuckFields,
|
|
43
|
+
schemaToTsType,
|
|
44
|
+
schemaToTsTypes,
|
|
45
|
+
slugToTableName
|
|
46
|
+
} from "./index-a3pnt8yz.js";
|
|
47
|
+
import {
|
|
48
|
+
MEDIA_FIELD_NAMES,
|
|
49
|
+
MediaFields,
|
|
50
|
+
MediaRef,
|
|
51
|
+
PLUGIN_CAPABILITIES,
|
|
52
|
+
REPO_MEDIA_FIELD_NAMES,
|
|
53
|
+
assertPluginCapabilities,
|
|
54
|
+
defineBlock,
|
|
55
|
+
defineCollection,
|
|
56
|
+
defineConfig,
|
|
57
|
+
definePage,
|
|
58
|
+
definePageTemplate,
|
|
59
|
+
definePlugin,
|
|
60
|
+
deriveContributedCapabilities,
|
|
61
|
+
mediaRepoName,
|
|
62
|
+
mediaTransformUrl,
|
|
63
|
+
resolveMediaRef,
|
|
64
|
+
stageRepoMedia,
|
|
65
|
+
withMediaFields
|
|
66
|
+
} from "./index-8g8ymd37.js";
|
|
67
|
+
// src/theme/index.ts
|
|
68
|
+
function defineTheme(opts) {
|
|
69
|
+
return opts;
|
|
70
|
+
}
|
|
71
|
+
function themeFingerprint(schemeId, darkMode, tenantScope) {
|
|
72
|
+
const schemeSegment = schemeId != null ? `id:${schemeId}` : "none";
|
|
73
|
+
const darkSegment = darkMode ? "dark" : "light";
|
|
74
|
+
const tenantSegment = tenantScope != null ? `:t:${tenantScope}` : "";
|
|
75
|
+
return `scheme:${schemeSegment}:${darkSegment}${tenantSegment}`;
|
|
76
|
+
}
|
|
77
|
+
// src/host/index.ts
|
|
78
|
+
var SCHEME_COOKIE = "saacms-scheme";
|
|
79
|
+
var DARK_MODE_COOKIE = "saacms-dark";
|
|
80
|
+
// src/publish/compile.ts
|
|
81
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
82
|
+
import { dirname, resolve } from "node:path";
|
|
83
|
+
function isCompileAdapter(host) {
|
|
84
|
+
if (typeof host !== "object" || host === null)
|
|
85
|
+
return false;
|
|
86
|
+
const h = host;
|
|
87
|
+
return typeof h.assetRoot === "function" && typeof h.generateRoute === "function";
|
|
88
|
+
}
|
|
89
|
+
async function compileForPublish(config, opts) {
|
|
90
|
+
const { cwd, dryRun = false } = opts;
|
|
91
|
+
const routes = [];
|
|
92
|
+
const content = [];
|
|
93
|
+
const media = [];
|
|
94
|
+
const adapter = isCompileAdapter(config.host) ? config.host : null;
|
|
95
|
+
for (const page of config.pages ?? []) {
|
|
96
|
+
const pageId = String(page.id);
|
|
97
|
+
const pageLayout = opts.readPageContent ? opts.readPageContent(pageId) : page.layout;
|
|
98
|
+
const contentRelPath = `content/pages/${pageId}.json`;
|
|
99
|
+
if (!dryRun) {
|
|
100
|
+
writeFileSafe(resolve(cwd, contentRelPath), cwd, JSON.stringify({ id: pageId, path: page.path, layout: pageLayout }, null, 2));
|
|
101
|
+
}
|
|
102
|
+
content.push(contentRelPath);
|
|
103
|
+
if (adapter !== null) {
|
|
104
|
+
const rawPath = String(page.path);
|
|
105
|
+
const url = rawPath.startsWith("/") ? rawPath.slice(1) : rawPath;
|
|
106
|
+
const generated = adapter.generateRoute({
|
|
107
|
+
id: pageId,
|
|
108
|
+
url,
|
|
109
|
+
renderMode: page.renderMode ?? "static",
|
|
110
|
+
layout: pageLayout
|
|
111
|
+
}, {});
|
|
112
|
+
if (!dryRun) {
|
|
113
|
+
writeFileSafe(resolve(cwd, generated.path), cwd, generated.source);
|
|
114
|
+
}
|
|
115
|
+
routes.push(generated.path);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const assetRoot = adapter?.assetRoot() ?? "public/";
|
|
119
|
+
for (const coll of config.collections ?? []) {
|
|
120
|
+
if (coll.kind !== "media")
|
|
121
|
+
continue;
|
|
122
|
+
const storage = coll.storage;
|
|
123
|
+
if (!storage || storage.mode !== "repo")
|
|
124
|
+
continue;
|
|
125
|
+
const slug = String(coll.slug);
|
|
126
|
+
const records = opts.readRepoMedia?.(slug) ?? [];
|
|
127
|
+
for (const record of records) {
|
|
128
|
+
const staged = await stageRepoMedia({
|
|
129
|
+
collectionSlug: slug,
|
|
130
|
+
assetRoot,
|
|
131
|
+
bytes: record.bytes,
|
|
132
|
+
mime: record.mime,
|
|
133
|
+
originalFilename: record.originalFilename,
|
|
134
|
+
policy: storage.naming
|
|
135
|
+
});
|
|
136
|
+
if (!dryRun) {
|
|
137
|
+
writeFileSafe(resolve(cwd, staged.repoPath), cwd, staged.bytes);
|
|
138
|
+
}
|
|
139
|
+
media.push(staged.repoPath);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const written = [...routes, ...content, ...media];
|
|
143
|
+
return { written, routes, content, media };
|
|
144
|
+
}
|
|
145
|
+
function writeFileSafe(absPath, cwd, data) {
|
|
146
|
+
const normalized = resolve(absPath);
|
|
147
|
+
const cwdNorm = resolve(cwd);
|
|
148
|
+
if (!normalized.startsWith(cwdNorm + "/")) {
|
|
149
|
+
throw new Error(`compileForPublish: refusing to write outside cwd "${cwdNorm}": ${absPath}`);
|
|
150
|
+
}
|
|
151
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
152
|
+
writeFileSync(absPath, data);
|
|
153
|
+
}
|
|
154
|
+
// src/tenant/index.ts
|
|
155
|
+
import { Effect, SchemaAST } from "effect";
|
|
156
|
+
function tenantScoped(opts) {
|
|
157
|
+
const { userField, recordField = userField } = opts;
|
|
158
|
+
const read = ({ user }) => {
|
|
159
|
+
if (user == null)
|
|
160
|
+
return false;
|
|
161
|
+
const tenantValue = user.claims[userField];
|
|
162
|
+
if (tenantValue == null)
|
|
163
|
+
return false;
|
|
164
|
+
return { where: { [recordField]: tenantValue } };
|
|
165
|
+
};
|
|
166
|
+
const beforeChange = (ctx) => Effect.sync(() => {
|
|
167
|
+
if (ctx.user == null)
|
|
168
|
+
return;
|
|
169
|
+
const tenantValue = ctx.user.claims[userField];
|
|
170
|
+
if (tenantValue == null)
|
|
171
|
+
return;
|
|
172
|
+
const data = ctx.data;
|
|
173
|
+
if (data != null) {
|
|
174
|
+
data[recordField] = tenantValue;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return { read, hooks: { beforeChange } };
|
|
178
|
+
}
|
|
179
|
+
function assertTenantIsolation(config, opts) {
|
|
180
|
+
const { tenantField, exempt = [] } = opts;
|
|
181
|
+
const exemptSet = new Set(exempt);
|
|
182
|
+
const unscoped = [];
|
|
183
|
+
for (const coll of config.collections ?? []) {
|
|
184
|
+
const slug = String(coll.slug);
|
|
185
|
+
if (exemptSet.has(slug))
|
|
186
|
+
continue;
|
|
187
|
+
if (!schemaHasField(coll.schema, tenantField))
|
|
188
|
+
continue;
|
|
189
|
+
if (coll.access?.read == null) {
|
|
190
|
+
unscoped.push(slug);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { ok: unscoped.length === 0, unscoped };
|
|
194
|
+
}
|
|
195
|
+
function schemaHasField(schema, fieldName) {
|
|
196
|
+
const ast = schema.ast;
|
|
197
|
+
if (!SchemaAST.isTypeLiteral(ast))
|
|
198
|
+
return false;
|
|
199
|
+
return ast.propertySignatures.some((p) => String(p.name) === fieldName);
|
|
200
|
+
}
|
|
201
|
+
export {
|
|
202
|
+
withMediaFields,
|
|
203
|
+
themeFingerprint,
|
|
204
|
+
tenantScoped,
|
|
205
|
+
stageRepoMedia,
|
|
206
|
+
slugToTableName,
|
|
207
|
+
signal,
|
|
208
|
+
schemaToTsTypes,
|
|
209
|
+
schemaToTsType,
|
|
210
|
+
schemaToPuckFields,
|
|
211
|
+
schemaToOpenApiSchema,
|
|
212
|
+
schemaToDrizzle,
|
|
213
|
+
schemaToD1Migrations,
|
|
214
|
+
schemaToD1Migration,
|
|
215
|
+
runHooks,
|
|
216
|
+
resolveMediaRef,
|
|
217
|
+
resolveHooksFor,
|
|
218
|
+
recordAuditEvent,
|
|
219
|
+
normalizeDateSchemas,
|
|
220
|
+
mediaTransformUrl,
|
|
221
|
+
mediaRepoName,
|
|
222
|
+
id,
|
|
223
|
+
getServices,
|
|
224
|
+
fingerprintToString,
|
|
225
|
+
fingerprintBlockSchema,
|
|
226
|
+
filterOpenApiForUser,
|
|
227
|
+
evaluateAccess,
|
|
228
|
+
effect,
|
|
229
|
+
diffBlockSchemas,
|
|
230
|
+
detectPendingContentMigrations,
|
|
231
|
+
deriveContributedCapabilities,
|
|
232
|
+
defineTheme,
|
|
233
|
+
definePlugin,
|
|
234
|
+
definePageTemplate,
|
|
235
|
+
definePage,
|
|
236
|
+
defineConfig,
|
|
237
|
+
defineCollection,
|
|
238
|
+
defineBlock,
|
|
239
|
+
defaultLabel,
|
|
240
|
+
createSaacmsRuntime,
|
|
241
|
+
contentMigrationModuleSource,
|
|
242
|
+
computed,
|
|
243
|
+
computeScaleCostSignal,
|
|
244
|
+
compileForPublish,
|
|
245
|
+
collectionToOpenApiPaths,
|
|
246
|
+
collectServices,
|
|
247
|
+
camelToSnakeCase,
|
|
248
|
+
assertTenantIsolation,
|
|
249
|
+
assertPluginCapabilities,
|
|
250
|
+
assertNoTableNameCollisions,
|
|
251
|
+
UniqueConstraintError,
|
|
252
|
+
SCHEME_COOKIE,
|
|
253
|
+
RowStorageError,
|
|
254
|
+
REPO_MEDIA_FIELD_NAMES,
|
|
255
|
+
PLUGIN_CAPABILITIES,
|
|
256
|
+
MediaRef,
|
|
257
|
+
MediaFields,
|
|
258
|
+
MEDIA_FIELD_NAMES,
|
|
259
|
+
DEFAULT_THRESHOLDS,
|
|
260
|
+
DARK_MODE_COOKIE
|
|
261
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization audit emit seam — ADR 0031.
|
|
3
|
+
*
|
|
4
|
+
* Provides the `AuditEvent` type, the `AuditSink` interface, and the
|
|
5
|
+
* `recordAuditEvent` helper. Persistence (D1 table / SIEM) is host-configured
|
|
6
|
+
* and OUT OF SCOPE for this module — this module is the *emit seam + interface*,
|
|
7
|
+
* not a storage backend.
|
|
8
|
+
*
|
|
9
|
+
* The default sink is a no-op (zero-config, never throws). The host or a plugin
|
|
10
|
+
* supplies a real sink by contributing `services: { auditSink: <AuditSink> }`
|
|
11
|
+
* in a `PluginDef` — exactly the same pattern as `services.cache` and
|
|
12
|
+
* `services.realtime` (ADR 0014 / `runtime/services.ts`).
|
|
13
|
+
*
|
|
14
|
+
* Per ADR 0020 §Observability, the Observability bounded context never owns
|
|
15
|
+
* business state; it receives domain events via a thin service interface. This
|
|
16
|
+
* module IS that thin interface for authorization decisions.
|
|
17
|
+
*/
|
|
18
|
+
import type { Context } from "hono";
|
|
19
|
+
import type { User } from "../types/user.ts";
|
|
20
|
+
/**
|
|
21
|
+
* A single authorization-decision audit record.
|
|
22
|
+
*
|
|
23
|
+
* Field rationale (ADR 0027 §9 `{ op, user, collection, result, recordId }`
|
|
24
|
+
* extended with `decision` + `at`):
|
|
25
|
+
* - `op` — which CRUD verb was attempted
|
|
26
|
+
* - `collection` — which Collection was targeted
|
|
27
|
+
* - `user` — the authenticated principal (null = anonymous)
|
|
28
|
+
* - `decision` — the binary outcome of the access predicate
|
|
29
|
+
* - `reason` — human-readable context (primarily useful for `denied`)
|
|
30
|
+
* - `recordId` — present when the predicate evaluated at record grain
|
|
31
|
+
* - `at` — ISO 8601 wall-clock timestamp of the decision
|
|
32
|
+
*/
|
|
33
|
+
export interface AuditEvent {
|
|
34
|
+
readonly op: "read" | "list" | "create" | "update" | "put" | "delete";
|
|
35
|
+
readonly collection: string;
|
|
36
|
+
readonly user: User | null;
|
|
37
|
+
readonly decision: "granted" | "denied";
|
|
38
|
+
readonly reason?: string;
|
|
39
|
+
readonly recordId?: string;
|
|
40
|
+
readonly at: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Host-supplied sink. The host or a plugin provides a concrete implementation
|
|
44
|
+
* via `PluginDef.services.auditSink`. Persistence (D1 table, SIEM, log
|
|
45
|
+
* pipeline) is entirely host-configured — this module ships no backend.
|
|
46
|
+
*/
|
|
47
|
+
export interface AuditSink {
|
|
48
|
+
emit(e: AuditEvent): void | Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Emit an authorization audit event via the injected sink, best-effort.
|
|
52
|
+
*
|
|
53
|
+
* Best-effort contract (mirrors event-phase hooks, ADR 0013):
|
|
54
|
+
* - A synchronous sink throw is caught and logged; it NEVER propagates to
|
|
55
|
+
* the caller or alters the HTTP response.
|
|
56
|
+
* - An async sink rejection is caught via `.catch`; it NEVER propagates.
|
|
57
|
+
* - This function returns synchronously; it NEVER blocks or delays the
|
|
58
|
+
* request regardless of sink latency.
|
|
59
|
+
*
|
|
60
|
+
* The sink is resolved from `getServices(c).auditSink`; when no sink is
|
|
61
|
+
* injected (the common case), the frozen no-op sink is used — zero overhead,
|
|
62
|
+
* zero config required.
|
|
63
|
+
*/
|
|
64
|
+
export declare function recordAuditEvent(c: Context, event: AuditEvent): void;
|
|
65
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/observability/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAE5C;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,QAAQ,CAAA;IACrE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,SAAS,GAAG,QAAQ,CAAA;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,CAAC,EAAE,UAAU,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1C;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAepE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/observability/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publish compile step — the "compile" half of `saacms publish`.
|
|
3
|
+
*
|
|
4
|
+
* Given a resolved `SaacmsConfig`, emits all artifacts the Publish pipeline
|
|
5
|
+
* commits to the developer's repo:
|
|
6
|
+
*
|
|
7
|
+
* 1. Host route files (e.g. `src/pages/<url>.astro`) for each Page in
|
|
8
|
+
* `config.pages`, generated via the host adapter.
|
|
9
|
+
* 2. Published content JSON (`content/pages/<id>.json`) for each Page —
|
|
10
|
+
* the page's Puck/Block tree persisted as a deterministic JSON file
|
|
11
|
+
* alongside the route in the commit.
|
|
12
|
+
* 3. Repo-mode media bytes (`<assetRoot>saacms/<slug>/<name>`) for each
|
|
13
|
+
* record in a `kind:"media", storage:{mode:"repo"}` Collection.
|
|
14
|
+
* Bucket-mode media lives in R2 and is never committed (ADR 0024).
|
|
15
|
+
*
|
|
16
|
+
* Pure w.r.t. injected readers: the CLI supplies real adapters that read from
|
|
17
|
+
* the live DB / R2; tests inject in-process stubs. The compile step itself
|
|
18
|
+
* only *writes* — it has no direct coupling to the live storage tier.
|
|
19
|
+
*
|
|
20
|
+
* Per ADR 0001 (compile step), ADR 0003 (saacms owns routing), ADR 0009
|
|
21
|
+
* (repo-mode media on Publish).
|
|
22
|
+
*/
|
|
23
|
+
import type { SaacmsConfig } from "../schema/index.ts";
|
|
24
|
+
/** The manifest returned by `compileForPublish`. */
|
|
25
|
+
export interface CompileManifest {
|
|
26
|
+
/** All paths written (routes + content + media). Relative to `opts.cwd`. */
|
|
27
|
+
readonly written: ReadonlyArray<string>;
|
|
28
|
+
/** Emitted host route files (e.g. `src/pages/about.astro`). */
|
|
29
|
+
readonly routes: ReadonlyArray<string>;
|
|
30
|
+
/** Emitted published content JSON files (e.g. `content/pages/p-about.json`). */
|
|
31
|
+
readonly content: ReadonlyArray<string>;
|
|
32
|
+
/** Emitted repo-mode media files (e.g. `public/saacms/images/abc.png`). */
|
|
33
|
+
readonly media: ReadonlyArray<string>;
|
|
34
|
+
}
|
|
35
|
+
/** One repo-mode media record to materialize on Publish. */
|
|
36
|
+
export interface RepoMediaRecord {
|
|
37
|
+
readonly bytes: Uint8Array;
|
|
38
|
+
readonly mime: string;
|
|
39
|
+
readonly originalFilename?: string;
|
|
40
|
+
}
|
|
41
|
+
/** Options for `compileForPublish`. */
|
|
42
|
+
export interface CompileOptions {
|
|
43
|
+
/** Absolute path to the project root (files are written here). */
|
|
44
|
+
readonly cwd: string;
|
|
45
|
+
/**
|
|
46
|
+
* When `true`, compute the manifest but do NOT write files to disk.
|
|
47
|
+
* Used by `saacms publish --dry` to report what would be emitted.
|
|
48
|
+
* Default: `false`.
|
|
49
|
+
*/
|
|
50
|
+
readonly dryRun?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Inject the Puck/Block layout for a given page id. When omitted, the
|
|
53
|
+
* compile step uses `page.layout` from the config directly. Allows the CLI
|
|
54
|
+
* to supply the live-DB version of the layout while tests supply a stub.
|
|
55
|
+
*/
|
|
56
|
+
readonly readPageContent?: (pageId: string) => unknown;
|
|
57
|
+
/**
|
|
58
|
+
* Inject repo-mode media records for a given collection slug. Each record
|
|
59
|
+
* carries the raw bytes, MIME type, and optional original filename. When
|
|
60
|
+
* omitted, no media is written (the collection has no pending uploads).
|
|
61
|
+
*/
|
|
62
|
+
readonly readRepoMedia?: (collectionSlug: string) => ReadonlyArray<RepoMediaRecord>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Emit all publish artifacts for the given `SaacmsConfig` into `opts.cwd`.
|
|
66
|
+
*
|
|
67
|
+
* Files are written deterministically: same config + same content → same
|
|
68
|
+
* bytes at the same paths. The compile step never writes outside `opts.cwd`
|
|
69
|
+
* (path-safety enforced by `writeFileSafe`).
|
|
70
|
+
*
|
|
71
|
+
* Returns a manifest of all written (or, in dryRun mode, would-be-written)
|
|
72
|
+
* paths relative to `opts.cwd`. The CLI passes these exact paths to
|
|
73
|
+
* `git add` so only saacms-emitted artifacts are staged.
|
|
74
|
+
*/
|
|
75
|
+
export declare function compileForPublish(config: SaacmsConfig, opts: CompileOptions): Promise<CompileManifest>;
|
|
76
|
+
//# sourceMappingURL=compile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/publish/compile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AA+BtD,oDAAoD;AACpD,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACvC,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACtC,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACvC,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACtC;AAED,4DAA4D;AAC5D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CACnC;AAED,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;IACtD;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,cAAc,EAAE,MAAM,KACnB,aAAa,CAAC,eAAe,CAAC,CAAA;CACpC;AAMD;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,eAAe,CAAC,CA6E1B"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth middleware — resolves the calling `User` once per request and stashes
|
|
3
|
+
* it under `c.set("user", ...)` so downstream handlers can read it.
|
|
4
|
+
*
|
|
5
|
+
* Per ADR 0008 + ADR 0020 (Identity bounded context), the runtime never imports
|
|
6
|
+
* a concrete auth library; the host wires a `config.auth: AuthAdapter`, and
|
|
7
|
+
* this middleware bridges it into the Hono request lifecycle.
|
|
8
|
+
*
|
|
9
|
+
* Mounted FIRST in `runtime/index.ts` so all downstream route handlers see a
|
|
10
|
+
* resolved user (or `undefined` for anonymous) when they read `c.get("user")`.
|
|
11
|
+
*
|
|
12
|
+
* Contract:
|
|
13
|
+
* - `config.auth` undefined → no middleware is registered at all
|
|
14
|
+
* (anonymous-only mode; the runtime stays usable before the host wires auth).
|
|
15
|
+
* - Otherwise, for every request:
|
|
16
|
+
* 1. call `config.auth.getSession(c.req.raw)` (the underlying Web Request),
|
|
17
|
+
* 2. on non-null `User` → `c.set("user", user)`,
|
|
18
|
+
* 3. on `null` → leave the variable unset,
|
|
19
|
+
* 4. on thrown exception → log via `console.warn` with the `[saacms/auth]`
|
|
20
|
+
* prefix and leave the variable unset.
|
|
21
|
+
* - The middleware NEVER rejects a request. Auth-required routes emit 401/403
|
|
22
|
+
* themselves via the `access` predicate.
|
|
23
|
+
*/
|
|
24
|
+
import type { Context, Env, Hono } from "hono";
|
|
25
|
+
import type { SaacmsConfig } from "../schema/index.ts";
|
|
26
|
+
import type { User } from "../types/user.ts";
|
|
27
|
+
export declare function mountAuthMiddleware<E extends Env>(app: Hono<E>, config: SaacmsConfig): void;
|
|
28
|
+
/**
|
|
29
|
+
* Read the user that the middleware stashed onto the context, or `null` for
|
|
30
|
+
* anonymous. The route handlers currently inline this cast; this helper exists
|
|
31
|
+
* so future refactors can converge on a single read site.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getUser(c: Context): User | null;
|
|
34
|
+
//# sourceMappingURL=auth-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-middleware.d.ts","sourceRoot":"","sources":["../../src/runtime/auth-middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAI5C,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,GAAG,EAC/C,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,EACZ,MAAM,EAAE,YAAY,GACnB,IAAI,CAuBN;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAG/C"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-driven Boolean ↔ INTEGER (de)serialisation at the runtime storage
|
|
3
|
+
* boundary.
|
|
4
|
+
*
|
|
5
|
+
* Per ADR 0005 the Effect Schema is the single source of truth; per ADR 0020
|
|
6
|
+
* the storage adapter is a schema-AGNOSTIC anti-corruption layer that only
|
|
7
|
+
* knows snake↔camel and never the column types. SQLite has no boolean type, so
|
|
8
|
+
* `to-d1-migration.ts` maps `Schema.Boolean` to an `INTEGER` column. The
|
|
9
|
+
* projection of a `boolean` domain field to its `0`/`1` integer cell (and
|
|
10
|
+
* back) therefore cannot live in the adapter — it lives here, at the runtime
|
|
11
|
+
* boundary, where the collection's Effect Schema is already in hand. This is
|
|
12
|
+
* the exact sibling of `json-columns.ts` for the scalar boolean case.
|
|
13
|
+
*
|
|
14
|
+
* - `encodeBooleanColumns` — map `true→1` / `false→0` for the boolean-typed
|
|
15
|
+
* fields of a record immediately BEFORE `rows.insert/update`.
|
|
16
|
+
* - `decodeBooleanColumns` — map the stored `1`/`0` (SQLite may surface it as
|
|
17
|
+
* a number OR a bigint) back to `true`/`false` immediately AFTER
|
|
18
|
+
* `rows.getById/list`.
|
|
19
|
+
*
|
|
20
|
+
* Without `decodeBooleanColumns` a `Schema.Boolean` create persists `1`/`0`
|
|
21
|
+
* but reads back as a *number*, so a subsequent PATCH (which re-validates the
|
|
22
|
+
* merged record against the Effect Schema) sees `1` where `boolean` is
|
|
23
|
+
* expected → a validation error. This is the identical failure shape the JSON
|
|
24
|
+
* gap had.
|
|
25
|
+
*
|
|
26
|
+
* Contract (identical to `json-columns.ts`):
|
|
27
|
+
* - Which fields are "boolean" is derived from the SAME AST the codegen walks
|
|
28
|
+
* (post optional/nullable/refinement stripping) via `AST.isBooleanKeyword`
|
|
29
|
+
* — the exact test `to-d1-migration.ts` uses to choose `INTEGER`, so codec
|
|
30
|
+
* and DDL can never disagree. Non-boolean primitives and the complex
|
|
31
|
+
* shapes `json-columns.ts` owns are never touched (disjoint by AST kind).
|
|
32
|
+
* - `null` / absent values are left untouched, composing correctly with the
|
|
33
|
+
* adapter's NULL→absent read behaviour (ADR 0020) — an absent optional
|
|
34
|
+
* boolean never becomes `0`/`false`.
|
|
35
|
+
* - Symmetric + idempotent for valid data: `decode∘encode = identity`;
|
|
36
|
+
* `decode` of an already-decoded (boolean) value is a no-op, so it is safe
|
|
37
|
+
* to run on a cache-hit raw record or a freshly-decoded one.
|
|
38
|
+
* - Pure: never mutates its input record; returns a shallow copy.
|
|
39
|
+
*
|
|
40
|
+
* Composition with `json-columns.ts`: the two codecs operate on disjoint field
|
|
41
|
+
* sets (boolean keyword vs. tuple/struct/unknown/any/object), so on the write
|
|
42
|
+
* path the order of `encodeBooleanColumns`/`encodeJsonColumns` is irrelevant,
|
|
43
|
+
* and likewise for the decode pair on the read path. The disjointness is
|
|
44
|
+
* asserted by a test.
|
|
45
|
+
*/
|
|
46
|
+
import type { AnySchema } from "../schema/index.ts";
|
|
47
|
+
/**
|
|
48
|
+
* Return a shallow copy of `record` with every boolean-typed field mapped
|
|
49
|
+
* `true→1` / `false→0`, ready for `rows.insert/update`. `null`/absent fields
|
|
50
|
+
* are passed through unchanged so the adapter's NULL handling still applies.
|
|
51
|
+
* A non-boolean value (e.g. an already-encoded `0`/`1`) is left as-is, which
|
|
52
|
+
* keeps `encode` idempotent.
|
|
53
|
+
*/
|
|
54
|
+
export declare function encodeBooleanColumns<T extends Record<string, unknown>>(schema: AnySchema, record: T): T;
|
|
55
|
+
/**
|
|
56
|
+
* Return a shallow copy of `record` with every boolean-typed field mapped from
|
|
57
|
+
* its stored integer back to a real `boolean`. SQLite may surface the cell as
|
|
58
|
+
* a number OR a bigint, so `1`/`1n`→`true` and `0`/`0n`→`false`. Values that
|
|
59
|
+
* are already boolean (already decoded) or `null`/absent are left untouched,
|
|
60
|
+
* so this is idempotent and safe to run on a raw storage row OR an
|
|
61
|
+
* already-decoded one. Any other value is left unchanged (defensive — never
|
|
62
|
+
* throw on a read path).
|
|
63
|
+
*/
|
|
64
|
+
export declare function decodeBooleanColumns<T extends Record<string, unknown>>(schema: AnySchema, record: T): T;
|
|
65
|
+
//# sourceMappingURL=boolean-columns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boolean-columns.d.ts","sourceRoot":"","sources":["../../src/runtime/boolean-columns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAmDnD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpE,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,CAAC,GACR,CAAC,CAWH;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpE,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,CAAC,GACR,CAAC,CAiBH"}
|