ignotum 0.0.5 → 0.0.7
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/dist/cli/bin.mjs +752 -84
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/runtime/{api-D2bsAz4T.d.ts → api-Dp4J-xt-.d.ts} +2 -3
- package/dist/runtime/client.d.ts +20 -3
- package/dist/runtime/client.js +217 -24
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/{descriptor-t6BOEGw9-BTHR-ZMv.js → descriptor-t6BOEGw9-C1rwYIlx.js} +29 -2
- package/dist/runtime/descriptor-t6BOEGw9-C1rwYIlx.js.map +1 -0
- package/dist/runtime/{result-BgcHn25t.d.ts → id-Btwac71X-DhnKYsjY.d.ts} +8 -2
- package/dist/runtime/{index-Dl_VQGmA.d.ts → index-B5KSOjGN.d.ts} +60 -69
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/host.d.ts +20 -4
- package/dist/runtime/internal/host.js +190 -7
- package/dist/runtime/internal/host.js.map +1 -1
- package/dist/runtime/internal/server.d.ts +1 -1
- package/dist/runtime/internal/server.js +1 -1
- package/dist/runtime/internal/types.d.ts +1 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/pagination-B1BzNkh8-BUSTbeSg.d.ts +80 -0
- package/dist/runtime/pagination-BKPko9Hm.d.ts +1 -0
- package/dist/runtime/{schema-B-jMZEbs.js → schema-D9RmboaS.js} +93 -29
- package/dist/runtime/schema-D9RmboaS.js.map +1 -0
- package/dist/runtime/server.d.ts +2 -2
- package/dist/runtime/server.js +2 -2
- package/package.json +3 -3
- package/src/cli/agent-files.ts +4 -0
- package/src/cli/build/client.ts +23 -15
- package/src/cli/build/public.ts +6 -2
- package/src/cli/deploy.ts +1 -1
- package/src/client/hooks.ts +164 -2
- package/src/client/index.ts +2 -1
- package/src/dev-runtime/database.ts +568 -37
- package/src/dev-runtime/migrations.ts +30 -0
- package/dist/runtime/descriptor-t6BOEGw9-BTHR-ZMv.js.map +0 -1
- package/dist/runtime/id-Btwac71X-B9OeBzvq.d.ts +0 -9
- package/dist/runtime/schema-B-jMZEbs.js.map +0 -1
package/dist/cli/bin.mjs
CHANGED
|
@@ -29650,6 +29650,10 @@ Function$1.dual(2, (value, descriptor) => {
|
|
|
29650
29650
|
});
|
|
29651
29651
|
return value;
|
|
29652
29652
|
});
|
|
29653
|
+
const getValueDescriptor = (value) => {
|
|
29654
|
+
if (!Predicate.hasProperty(value, ValueDescriptorTypeId)) return void 0;
|
|
29655
|
+
return Schema.is(ValueDescriptor)(value[ValueDescriptorTypeId]) ? value[ValueDescriptorTypeId] : void 0;
|
|
29656
|
+
};
|
|
29653
29657
|
//#endregion
|
|
29654
29658
|
//#region ../contracts/dist/deployment.js
|
|
29655
29659
|
const ArtifactPath = Schema.String.check(Schema.isPattern(/^(?!\/)(?![A-Za-z]:\/)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*(?:^|\/)\.(?:\/|$))(?!.*\/\/)[^\\\0]+$/)).pipe(Schema.brand("ignotum/deployment/ArtifactPath"));
|
|
@@ -29658,6 +29662,7 @@ const Sha256 = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/)).pipe(Sche
|
|
|
29658
29662
|
const ArtifactKind = Schema.Literals([
|
|
29659
29663
|
"ClientAsset",
|
|
29660
29664
|
"ClientDocument",
|
|
29665
|
+
"ClientManifest",
|
|
29661
29666
|
"ClientPublicFile",
|
|
29662
29667
|
"ClientShell",
|
|
29663
29668
|
"FunctionBundle",
|
|
@@ -29680,23 +29685,41 @@ const ArtifactFile = Schema.Struct({
|
|
|
29680
29685
|
});
|
|
29681
29686
|
const ClientRoute = Schema.Struct({
|
|
29682
29687
|
pathname: ClientPath,
|
|
29683
|
-
artifact:
|
|
29688
|
+
artifact: ArtifactReference
|
|
29684
29689
|
});
|
|
29685
29690
|
const DeploymentInventory = Schema.Struct({
|
|
29686
29691
|
formatVersion: Schema.Literal(1),
|
|
29687
|
-
client: Schema.Struct({ routes: Schema.Array(ClientRoute) }),
|
|
29688
29692
|
files: Schema.Array(ArtifactFile)
|
|
29689
29693
|
});
|
|
29694
|
+
const ClientManifest = Schema.Struct({
|
|
29695
|
+
formatVersion: Schema.Literal(1),
|
|
29696
|
+
shell: ArtifactReference,
|
|
29697
|
+
routes: Schema.Array(ClientRoute)
|
|
29698
|
+
});
|
|
29699
|
+
const ClientRoutingRoute = Schema.Struct({
|
|
29700
|
+
pathname: ClientPath,
|
|
29701
|
+
artifact: ArtifactPath
|
|
29702
|
+
});
|
|
29703
|
+
Schema.Struct({
|
|
29704
|
+
formatVersion: Schema.Literal(1),
|
|
29705
|
+
shell: ArtifactPath,
|
|
29706
|
+
routes: Schema.Array(ClientRoutingRoute)
|
|
29707
|
+
});
|
|
29690
29708
|
const SchemaSnapshotField = Schema.Struct({
|
|
29691
29709
|
name: Schema.String,
|
|
29692
29710
|
value: ValueDescriptor
|
|
29693
29711
|
});
|
|
29712
|
+
const SchemaSnapshotIndex = Schema.Struct({
|
|
29713
|
+
name: Schema.String,
|
|
29714
|
+
fields: Schema.Array(Schema.String)
|
|
29715
|
+
});
|
|
29694
29716
|
const SchemaSnapshotTable = Schema.Struct({
|
|
29695
29717
|
name: Schema.String,
|
|
29696
|
-
fields: Schema.Array(SchemaSnapshotField)
|
|
29718
|
+
fields: Schema.Array(SchemaSnapshotField),
|
|
29719
|
+
indexes: Schema.Array(SchemaSnapshotIndex)
|
|
29697
29720
|
});
|
|
29698
29721
|
const SchemaSnapshot = Schema.Struct({
|
|
29699
|
-
formatVersion: Schema.Literal(
|
|
29722
|
+
formatVersion: Schema.Literal(2),
|
|
29700
29723
|
tables: Schema.Array(SchemaSnapshotTable)
|
|
29701
29724
|
});
|
|
29702
29725
|
const ServerFunctionArtifact = Schema.Struct({
|
|
@@ -29711,8 +29734,10 @@ const ServerBuildManifest = Schema.Struct({
|
|
|
29711
29734
|
functions: Schema.Array(ServerFunctionArtifact)
|
|
29712
29735
|
});
|
|
29713
29736
|
const deploymentInventoryPath = ArtifactPath.make("inventory.json");
|
|
29714
|
-
const
|
|
29715
|
-
const
|
|
29737
|
+
const clientManifestPath = ArtifactPath.make("client/manifest.json");
|
|
29738
|
+
const clientShellPath = ArtifactPath.make("client/shell.html");
|
|
29739
|
+
const clientAssetPathPrefix = `${ArtifactPath.make("client/assets")}/`;
|
|
29740
|
+
const clientRoutePathPrefix = `${ArtifactPath.make("client/routes")}/`;
|
|
29716
29741
|
const clientPublicFileExtensions = [
|
|
29717
29742
|
".avif",
|
|
29718
29743
|
".gif",
|
|
@@ -29823,7 +29848,7 @@ var IdGenerator = class IdGenerator extends Context.Service()("@ignotum/shared/i
|
|
|
29823
29848
|
};
|
|
29824
29849
|
//#endregion
|
|
29825
29850
|
//#region package.json
|
|
29826
|
-
var version = "0.0.
|
|
29851
|
+
var version = "0.0.7";
|
|
29827
29852
|
//#endregion
|
|
29828
29853
|
//#region src/cli/codegen.ts
|
|
29829
29854
|
const generatedHeader = "// Generated by `ignotum codegen`. Do not edit.";
|
|
@@ -30069,12 +30094,12 @@ const controlClientLayer = Layer.effect(ControlClient, Effect.gen(function* () {
|
|
|
30069
30094
|
}));
|
|
30070
30095
|
//#endregion
|
|
30071
30096
|
//#region ../contracts/dist/json.js
|
|
30072
|
-
const encodeScalar = (value) => Schema.decodeSync(Schema.String)(JSON.stringify(value));
|
|
30097
|
+
const encodeScalar$1 = (value) => Schema.decodeSync(Schema.String)(JSON.stringify(value));
|
|
30073
30098
|
const encodeCanonicalJson = (value) => {
|
|
30074
|
-
if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return encodeScalar(value);
|
|
30099
|
+
if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return encodeScalar$1(value);
|
|
30075
30100
|
if (Predicate.isObject(value)) return `{${Array$1.map(Array$1.sort(String$1.Order)(Object.keys(value)), (key) => {
|
|
30076
30101
|
const field = Schema.decodeUnknownSync(Schema.Json)(value[key]);
|
|
30077
|
-
return `${encodeScalar(key)}:${encodeCanonicalJson(field)}`;
|
|
30102
|
+
return `${encodeScalar$1(key)}:${encodeCanonicalJson(field)}`;
|
|
30078
30103
|
}).join(",")}}`;
|
|
30079
30104
|
return `[${Schema.decodeUnknownSync(Schema.Array(Schema.Json))(value).map(encodeCanonicalJson).join(",")}]`;
|
|
30080
30105
|
};
|
|
@@ -30157,6 +30182,58 @@ const documentNotFound = (table, id) => Brand.nominal()({
|
|
|
30157
30182
|
id
|
|
30158
30183
|
});
|
|
30159
30184
|
//#endregion
|
|
30185
|
+
//#region ../contracts/dist/runtime/pagination.js
|
|
30186
|
+
const PaginationCursor = Schema.String.check(Schema.isBase64Url()).pipe(Schema.brand("ignotum/runtime/PaginationCursor"));
|
|
30187
|
+
const PaginationPageSize = Schema.Int.check(Schema.isBetween({
|
|
30188
|
+
minimum: 1,
|
|
30189
|
+
maximum: 1e3
|
|
30190
|
+
})).pipe(Schema.brand("ignotum/runtime/PaginationPageSize"));
|
|
30191
|
+
Schema.Struct({
|
|
30192
|
+
cursor: Schema.NullOr(PaginationCursor),
|
|
30193
|
+
pageSize: PaginationPageSize
|
|
30194
|
+
});
|
|
30195
|
+
const TablePosition = Schema.Struct({
|
|
30196
|
+
type: Schema.Literal("Table"),
|
|
30197
|
+
createdAt: Schema.Int,
|
|
30198
|
+
id: Schema.String
|
|
30199
|
+
});
|
|
30200
|
+
const IndexPosition = Schema.Struct({
|
|
30201
|
+
type: Schema.Literal("Index"),
|
|
30202
|
+
key: Schema.String.check(Schema.isPattern(/^(?:[0-9a-f]{2})*$/)).pipe(Schema.brand("ignotum/runtime/EncodedIndexKey"))
|
|
30203
|
+
});
|
|
30204
|
+
const PaginationPosition = Schema.Union([TablePosition, IndexPosition]);
|
|
30205
|
+
const CursorPayload = Schema.Struct({
|
|
30206
|
+
version: Schema.Literal(1),
|
|
30207
|
+
query: Schema.String,
|
|
30208
|
+
position: PaginationPosition
|
|
30209
|
+
});
|
|
30210
|
+
const CursorPayloadJson = Schema.fromJsonString(CursorPayload);
|
|
30211
|
+
const paginationQueryIdentity = (tableId, index, lower, upper, order) => encodeCanonicalJson([
|
|
30212
|
+
tableId,
|
|
30213
|
+
index,
|
|
30214
|
+
lower ?? null,
|
|
30215
|
+
upper ?? null,
|
|
30216
|
+
order
|
|
30217
|
+
]);
|
|
30218
|
+
const encodePaginationCursor = (query, position) => PaginationCursor.make(Encoding.encodeBase64Url(Schema.encodeSync(CursorPayloadJson)({
|
|
30219
|
+
version: 1,
|
|
30220
|
+
query,
|
|
30221
|
+
position
|
|
30222
|
+
})));
|
|
30223
|
+
const decodePaginationCursor = (cursor) => Schema.decodeSync(CursorPayloadJson)(Result.getOrThrow(Encoding.decodeBase64UrlString(cursor)));
|
|
30224
|
+
Schema.Union([
|
|
30225
|
+
Schema.String,
|
|
30226
|
+
Schema.Finite,
|
|
30227
|
+
Schema.Boolean
|
|
30228
|
+
]);
|
|
30229
|
+
const schemaIndexLimits = {
|
|
30230
|
+
fieldsPerIndex: 8,
|
|
30231
|
+
indexesPerSchema: 128,
|
|
30232
|
+
indexesPerTable: 16,
|
|
30233
|
+
keyBytes: 4096,
|
|
30234
|
+
nameBytes: 64
|
|
30235
|
+
};
|
|
30236
|
+
//#endregion
|
|
30160
30237
|
//#region ../contracts/dist/runtime/schema.js
|
|
30161
30238
|
const FunctionSchemaTypeId = Symbol.for("ignotum/runtime/schema/FunctionSchema");
|
|
30162
30239
|
const getFunctionSchema = (value) => value[FunctionSchemaTypeId];
|
|
@@ -30221,11 +30298,11 @@ const contentType = (path) => {
|
|
|
30221
30298
|
};
|
|
30222
30299
|
const isClientPublicFile = (path) => clientPublicFileExtensions.some((extension) => path.toLowerCase().endsWith(extension));
|
|
30223
30300
|
const artifactKind = (path) => {
|
|
30301
|
+
if (path === clientManifestPath) return "ClientManifest";
|
|
30224
30302
|
if (path === clientShellPath) return "ClientShell";
|
|
30225
30303
|
if (path.startsWith(clientAssetPathPrefix)) return "ClientAsset";
|
|
30226
|
-
if (path.startsWith("
|
|
30227
|
-
if (path.startsWith(
|
|
30228
|
-
if (path.startsWith("client/") && isClientPublicFile(path)) return "ClientPublicFile";
|
|
30304
|
+
if (path.startsWith(clientRoutePathPrefix) && path.endsWith(".html")) return "ClientDocument";
|
|
30305
|
+
if (path.startsWith(clientRoutePathPrefix) && isClientPublicFile(path)) return "ClientPublicFile";
|
|
30229
30306
|
if (path === serverManifestPath) return "ServerManifest";
|
|
30230
30307
|
if (path === schemaSnapshotPath) return "SchemaSnapshot";
|
|
30231
30308
|
if (path.startsWith("server/functions/") && path.endsWith(".mjs.map")) return "SourceMap";
|
|
@@ -30242,18 +30319,16 @@ const makeArtifactFile = Effect.fn("Deployment.makeArtifactFile")(function* (inp
|
|
|
30242
30319
|
contentType: contentType(input.path)
|
|
30243
30320
|
};
|
|
30244
30321
|
});
|
|
30245
|
-
const makeInventoryFromNormalized = Effect.fn("Deployment.makeInventoryFromNormalized")(function* (files
|
|
30322
|
+
const makeInventoryFromNormalized = Effect.fn("Deployment.makeInventoryFromNormalized")(function* (files) {
|
|
30246
30323
|
const inventoryFiles = yield* Effect.forEach(files, makeArtifactFile, { concurrency: "unbounded" });
|
|
30247
30324
|
return {
|
|
30248
30325
|
formatVersion: 1,
|
|
30249
|
-
client: { routes: Array$1.sortWith(routes, (route) => route.pathname, String$1.Order) },
|
|
30250
30326
|
files: Array$1.sortWith(inventoryFiles, (file) => file.path, String$1.Order)
|
|
30251
30327
|
};
|
|
30252
30328
|
});
|
|
30253
30329
|
const validateDeploymentInventory = Effect.fn("Deployment.validateDeploymentInventory")(function* (inventory) {
|
|
30254
30330
|
if (inventory.files.length > deploymentArtifactLimits.fileCount) return yield* invalid$3(`Deployment inventories may contain at most ${deploymentArtifactLimits.fileCount} files.`);
|
|
30255
30331
|
const paths = /* @__PURE__ */ new Set();
|
|
30256
|
-
const entries = /* @__PURE__ */ new Map();
|
|
30257
30332
|
let previousPath;
|
|
30258
30333
|
let serverBytes = 0;
|
|
30259
30334
|
let totalBytes = 0;
|
|
@@ -30267,34 +30342,22 @@ const validateDeploymentInventory = Effect.fn("Deployment.validateDeploymentInve
|
|
|
30267
30342
|
}
|
|
30268
30343
|
if (paths.has(file.path)) return yield* invalid$3(`Duplicate deployment inventory path '${file.path}'.`, file.path);
|
|
30269
30344
|
paths.add(file.path);
|
|
30270
|
-
entries.set(file.path, file);
|
|
30271
30345
|
if (previousPath !== void 0 && String$1.Order(previousPath, file.path) >= 0) return yield* invalid$3("Deployment inventory files must be sorted by path.", file.path);
|
|
30272
30346
|
previousPath = file.path;
|
|
30273
30347
|
const expectedKind = artifactKind(file.path);
|
|
30274
30348
|
if (expectedKind === void 0 || expectedKind !== file.kind) return yield* invalid$3(`Deployment inventory kind '${file.kind}' does not match '${file.path}'.`, file.path);
|
|
30275
30349
|
}
|
|
30276
|
-
for (const path of [
|
|
30277
|
-
|
|
30278
|
-
|
|
30279
|
-
|
|
30280
|
-
|
|
30281
|
-
|
|
30282
|
-
const entry = entries.get(route.artifact);
|
|
30283
|
-
if (entry === void 0) return yield* invalid$3(`Client route '${route.pathname}' references missing artifact '${route.artifact}'.`, route.artifact);
|
|
30284
|
-
if (entry.kind !== "ClientDocument" && entry.kind !== "ClientPublicFile") return yield* invalid$3(`Client route '${route.pathname}' cannot reference artifact kind '${entry.kind}'.`, route.artifact);
|
|
30285
|
-
routedArtifacts.set(route.artifact, (routedArtifacts.get(route.artifact) ?? 0) + 1);
|
|
30286
|
-
}
|
|
30287
|
-
for (const file of inventory.files) {
|
|
30288
|
-
const routeCount = routedArtifacts.get(file.path) ?? 0;
|
|
30289
|
-
if (file.kind === "ClientDocument" && routeCount === 0) return yield* invalid$3(`Client document '${file.path}' has no route.`, file.path);
|
|
30290
|
-
if (file.kind === "ClientPublicFile" && routeCount !== 1) return yield* invalid$3(`Client public file '${file.path}' must have exactly one route.`, file.path);
|
|
30291
|
-
}
|
|
30292
|
-
if (!paths.has(clientShellPath) && inventory.client.routes.length === 0) return yield* invalid$3("A deployment must contain a client shell or an exact client route.");
|
|
30350
|
+
for (const path of [
|
|
30351
|
+
clientManifestPath,
|
|
30352
|
+
clientShellPath,
|
|
30353
|
+
serverManifestPath,
|
|
30354
|
+
schemaSnapshotPath
|
|
30355
|
+
]) if (!paths.has(path)) return yield* invalid$3(`Artifact file '${path}' is missing.`, path);
|
|
30293
30356
|
});
|
|
30294
|
-
const makeDeploymentInventory = Effect.fn("Deployment.makeDeploymentInventory")(function* (inputs
|
|
30357
|
+
const makeDeploymentInventory = Effect.fn("Deployment.makeDeploymentInventory")(function* (inputs) {
|
|
30295
30358
|
const files = yield* normalizeFiles(inputs);
|
|
30296
30359
|
if (files.some((file) => file.path === deploymentInventoryPath)) return yield* invalid$3(`${deploymentInventoryPath} cannot include itself in the deployment inventory.`, deploymentInventoryPath);
|
|
30297
|
-
const inventory = yield* makeInventoryFromNormalized(files
|
|
30360
|
+
const inventory = yield* makeInventoryFromNormalized(files);
|
|
30298
30361
|
const encoded = `${encodeCanonical(DeploymentInventory, inventory)}\n`;
|
|
30299
30362
|
return {
|
|
30300
30363
|
inventory,
|
|
@@ -30317,6 +30380,24 @@ const findFile = (files, path) => {
|
|
|
30317
30380
|
return file === void 0 ? Effect.fail(invalid$3(`Artifact file '${path}' is missing.`, path)) : Effect.succeed(file);
|
|
30318
30381
|
};
|
|
30319
30382
|
const sameReference = (entry, reference) => entry.path === reference.path && entry.size === reference.size && entry.sha256 === reference.sha256;
|
|
30383
|
+
const validateClientManifest = Effect.fn("Deployment.validateClientManifest")(function* (manifest, inventory) {
|
|
30384
|
+
if (manifest.shell.path !== clientShellPath) return yield* invalid$3(`The client manifest must reference '${clientShellPath}' as its shell.`, manifest.shell.path);
|
|
30385
|
+
const entries = new Map(inventory.files.map((file) => [file.path, file]));
|
|
30386
|
+
const shellEntry = entries.get(manifest.shell.path);
|
|
30387
|
+
if (shellEntry === void 0 || shellEntry.kind !== "ClientShell" || !sameReference(shellEntry, manifest.shell)) return yield* invalid$3(`The client shell reference for '${manifest.shell.path}' does not match the inventory.`, manifest.shell.path);
|
|
30388
|
+
const referencedPaths = /* @__PURE__ */ new Set();
|
|
30389
|
+
let previousRoute;
|
|
30390
|
+
for (const route of manifest.routes) {
|
|
30391
|
+
if (previousRoute !== void 0 && String$1.Order(previousRoute, route.pathname) >= 0) return yield* invalid$3("Client routes must be sorted by pathname.", route.pathname);
|
|
30392
|
+
previousRoute = route.pathname;
|
|
30393
|
+
if (referencedPaths.has(route.artifact.path)) return yield* invalid$3(`Client artifact path '${route.artifact.path}' is referenced more than once.`, route.artifact.path);
|
|
30394
|
+
referencedPaths.add(route.artifact.path);
|
|
30395
|
+
const entry = entries.get(route.artifact.path);
|
|
30396
|
+
if (entry === void 0 || !sameReference(entry, route.artifact)) return yield* invalid$3(`The client route reference for '${route.artifact.path}' does not match the inventory.`, route.artifact.path);
|
|
30397
|
+
if (entry.kind !== "ClientDocument" && entry.kind !== "ClientPublicFile") return yield* invalid$3(`Client route '${route.pathname}' cannot reference artifact kind '${entry.kind}'.`, route.artifact.path);
|
|
30398
|
+
}
|
|
30399
|
+
for (const file of inventory.files) if ((file.kind === "ClientDocument" || file.kind === "ClientPublicFile") && !referencedPaths.has(file.path)) return yield* invalid$3(`Client route artifact '${file.path}' has no route.`, file.path);
|
|
30400
|
+
});
|
|
30320
30401
|
const validateManifest = Effect.fn("Deployment.validateManifest")(function* (manifest, inventory) {
|
|
30321
30402
|
if (manifest.schema.path !== schemaSnapshotPath) return yield* invalid$3(`The server manifest must reference '${schemaSnapshotPath}'.`, manifest.schema.path);
|
|
30322
30403
|
const entries = new Map(inventory.files.map((file) => [file.path, file]));
|
|
@@ -30357,17 +30438,28 @@ Effect.fn("Deployment.validateDeploymentMetadata")(function* (input) {
|
|
|
30357
30438
|
path: schemaSnapshotPath,
|
|
30358
30439
|
bytes: input.schemaBytes
|
|
30359
30440
|
};
|
|
30360
|
-
const
|
|
30441
|
+
const clientManifestFile = {
|
|
30442
|
+
path: clientManifestPath,
|
|
30443
|
+
bytes: input.clientManifestBytes
|
|
30444
|
+
};
|
|
30445
|
+
const metadataFiles = [
|
|
30446
|
+
clientManifestFile,
|
|
30447
|
+
manifestFile,
|
|
30448
|
+
schemaFile
|
|
30449
|
+
];
|
|
30361
30450
|
const entries = new Map(input.inventory.files.map((file) => [file.path, file]));
|
|
30362
30451
|
for (const file of metadataFiles) {
|
|
30363
30452
|
const entry = entries.get(file.path);
|
|
30364
30453
|
const actual = yield* makeArtifactFile(file);
|
|
30365
30454
|
if (entry === void 0 || encodeCanonical(ArtifactFile, actual) !== encodeCanonical(ArtifactFile, entry)) return yield* invalid$3(`Artifact file '${file.path}' does not match the deployment inventory.`, file.path);
|
|
30366
30455
|
}
|
|
30456
|
+
const clientManifest = yield* decodeJsonFile(ClientManifest, clientManifestFile);
|
|
30367
30457
|
const manifest = yield* decodeJsonFile(ServerBuildManifest, manifestFile);
|
|
30368
30458
|
const schema = yield* decodeJsonFile(SchemaSnapshot, schemaFile);
|
|
30459
|
+
yield* validateClientManifest(clientManifest, input.inventory);
|
|
30369
30460
|
yield* validateManifest(manifest, input.inventory);
|
|
30370
30461
|
return {
|
|
30462
|
+
clientManifest,
|
|
30371
30463
|
inventory: input.inventory,
|
|
30372
30464
|
manifest,
|
|
30373
30465
|
schema
|
|
@@ -30379,14 +30471,18 @@ const validateDeploymentArtifact = Effect.fn("Deployment.validateDeploymentArtif
|
|
|
30379
30471
|
const payloadFiles = files.filter((file) => file.path !== deploymentInventoryPath);
|
|
30380
30472
|
const inventory = yield* decodeJsonFile(DeploymentInventory, inventoryFile);
|
|
30381
30473
|
yield* validateDeploymentInventory(inventory);
|
|
30382
|
-
const actualInventory = yield* makeInventoryFromNormalized(payloadFiles
|
|
30474
|
+
const actualInventory = yield* makeInventoryFromNormalized(payloadFiles);
|
|
30383
30475
|
if (encodeCanonical(DeploymentInventory, inventory) !== encodeCanonical(DeploymentInventory, actualInventory)) return yield* invalid$3("The deployment inventory does not match the artifact files.");
|
|
30476
|
+
const clientManifestFile = yield* findFile(files, clientManifestPath);
|
|
30384
30477
|
const manifestFile = yield* findFile(files, serverManifestPath);
|
|
30385
30478
|
const schemaFile = yield* findFile(files, schemaSnapshotPath);
|
|
30479
|
+
const clientManifest = yield* decodeJsonFile(ClientManifest, clientManifestFile);
|
|
30386
30480
|
const manifest = yield* decodeJsonFile(ServerBuildManifest, manifestFile);
|
|
30387
30481
|
const schema = yield* decodeJsonFile(SchemaSnapshot, schemaFile);
|
|
30482
|
+
yield* validateClientManifest(clientManifest, inventory);
|
|
30388
30483
|
yield* validateManifest(manifest, inventory);
|
|
30389
30484
|
return {
|
|
30485
|
+
clientManifest,
|
|
30390
30486
|
inventory,
|
|
30391
30487
|
manifest,
|
|
30392
30488
|
schema
|
|
@@ -30437,19 +30533,36 @@ const readArtifactDirectory = Effect.fn("Deployment.readArtifactDirectory")(func
|
|
|
30437
30533
|
const makeSchemaSnapshot = (schema) => {
|
|
30438
30534
|
const definition = schema[SchemaDefinitionTypeId];
|
|
30439
30535
|
return {
|
|
30440
|
-
formatVersion:
|
|
30536
|
+
formatVersion: 2,
|
|
30441
30537
|
tables: Array$1.map(Array$1.sort(String$1.Order)(Object.keys(definition)), (name) => {
|
|
30442
30538
|
const table = definition[name];
|
|
30443
30539
|
if (table === void 0 || table.descriptor.type !== "object") throw new Error(`The table '${name}' does not have a canonical Ignotum descriptor.`);
|
|
30540
|
+
const indexes = Object.values(table.indexes).map((index) => ({
|
|
30541
|
+
name: index.name,
|
|
30542
|
+
fields: globalThis.Array.from(index.fields)
|
|
30543
|
+
})).sort((left, right) => left.name.localeCompare(right.name));
|
|
30444
30544
|
return {
|
|
30445
30545
|
name,
|
|
30446
|
-
fields: table.descriptor.fields
|
|
30546
|
+
fields: table.descriptor.fields,
|
|
30547
|
+
indexes
|
|
30447
30548
|
};
|
|
30448
30549
|
})
|
|
30449
30550
|
};
|
|
30450
30551
|
};
|
|
30451
30552
|
const encodeSchemaSnapshot = (snapshot) => `${encodeCanonical(SchemaSnapshot, snapshot)}\n`;
|
|
30452
|
-
Function$1.dual(2, (previous, next) => previous === void 0 || encodeCanonical(SchemaSnapshot,
|
|
30553
|
+
Function$1.dual(2, (previous, next) => previous === void 0 || encodeCanonical(SchemaSnapshot, {
|
|
30554
|
+
...previous,
|
|
30555
|
+
tables: previous.tables.map((table) => ({
|
|
30556
|
+
...table,
|
|
30557
|
+
indexes: []
|
|
30558
|
+
}))
|
|
30559
|
+
}) === encodeCanonical(SchemaSnapshot, {
|
|
30560
|
+
...next,
|
|
30561
|
+
tables: next.tables.map((table) => ({
|
|
30562
|
+
...table,
|
|
30563
|
+
indexes: []
|
|
30564
|
+
}))
|
|
30565
|
+
}));
|
|
30453
30566
|
//#endregion
|
|
30454
30567
|
//#region src/cli/app-configuration.ts
|
|
30455
30568
|
const AppConfiguration = Schema.Struct({
|
|
@@ -31001,10 +31114,10 @@ const copyPublicFiles = Effect.fn("Deploy.copyPublicFiles")(function* (appDirect
|
|
|
31001
31114
|
const pathname = `/${relativePath}`;
|
|
31002
31115
|
return {
|
|
31003
31116
|
bytes,
|
|
31004
|
-
destination: path.join(outputDirectory, entry),
|
|
31117
|
+
destination: path.join(outputDirectory, "routes", entry),
|
|
31005
31118
|
route: {
|
|
31006
31119
|
pathname: ClientPath.make(pathname),
|
|
31007
|
-
artifact: ArtifactPath.make(`client/${relativePath}`)
|
|
31120
|
+
artifact: yield* artifactReference(ArtifactPath.make(`client/routes/${relativePath}`), bytes)
|
|
31008
31121
|
},
|
|
31009
31122
|
source
|
|
31010
31123
|
};
|
|
@@ -31035,15 +31148,15 @@ const buildClient = Effect.fn("Deploy.buildClient")(function* (appDirectory, out
|
|
|
31035
31148
|
const output = yield* runViteBuild("client", {
|
|
31036
31149
|
appType: "spa",
|
|
31037
31150
|
build: {
|
|
31038
|
-
assetsDir: "
|
|
31151
|
+
assetsDir: "assets",
|
|
31039
31152
|
emptyOutDir: true,
|
|
31040
31153
|
outDir: outputDirectory,
|
|
31041
31154
|
rolldownOptions: {
|
|
31042
31155
|
input: clientEntryId,
|
|
31043
31156
|
output: {
|
|
31044
|
-
assetFileNames: (asset) => asset.names.some((name) => name.endsWith(".css")) ? "
|
|
31045
|
-
chunkFileNames: "
|
|
31046
|
-
entryFileNames: "
|
|
31157
|
+
assetFileNames: (asset) => asset.names.some((name) => name.endsWith(".css")) ? "assets/styles-[hash][extname]" : "assets/[name]-[hash][extname]",
|
|
31158
|
+
chunkFileNames: "assets/chunks/[name]-[hash].js",
|
|
31159
|
+
entryFileNames: "assets/main-[hash].js"
|
|
31047
31160
|
}
|
|
31048
31161
|
},
|
|
31049
31162
|
sourcemap: false
|
|
@@ -31079,27 +31192,35 @@ const buildClient = Effect.fn("Deploy.buildClient")(function* (appDirectory, out
|
|
|
31079
31192
|
let icon;
|
|
31080
31193
|
if (clientFiles.iconPath !== void 0) {
|
|
31081
31194
|
const bytes = yield* fileSystem.readFile(clientFiles.iconPath);
|
|
31082
|
-
const fileName = `icon-${yield* sha256(bytes)}.svg`;
|
|
31083
|
-
const assetsDirectory = path.join(outputDirectory, "
|
|
31195
|
+
const fileName = `icon-${(yield* sha256(bytes)).slice(0, 16)}.svg`;
|
|
31196
|
+
const assetsDirectory = path.join(outputDirectory, "assets");
|
|
31084
31197
|
yield* fileSystem.makeDirectory(assetsDirectory, { recursive: true });
|
|
31085
31198
|
yield* fileSystem.writeFile(path.join(assetsDirectory, fileName), bytes);
|
|
31086
31199
|
icon = `/_ignotum/assets/${fileName}`;
|
|
31087
31200
|
}
|
|
31088
|
-
const shellPath = path.join(outputDirectory, "
|
|
31201
|
+
const shellPath = path.join(outputDirectory, "shell.html");
|
|
31089
31202
|
const document = renderClientDocument({
|
|
31090
31203
|
icon,
|
|
31091
31204
|
scripts: [{
|
|
31092
|
-
source: entryFile
|
|
31205
|
+
source: `_ignotum/${entryFile}`,
|
|
31093
31206
|
type: "External"
|
|
31094
31207
|
}],
|
|
31095
|
-
styles,
|
|
31208
|
+
styles: styles.map((style) => `_ignotum/${style}`),
|
|
31096
31209
|
title
|
|
31097
31210
|
});
|
|
31098
|
-
|
|
31211
|
+
const shellBytes = utf8Bytes(document);
|
|
31212
|
+
yield* fileSystem.writeFile(shellPath, shellBytes);
|
|
31099
31213
|
const publicBuild = yield* copyPublicFiles(appDirectory, outputDirectory);
|
|
31214
|
+
const manifest = {
|
|
31215
|
+
formatVersion: 1,
|
|
31216
|
+
shell: yield* artifactReference(clientShellPath, shellBytes),
|
|
31217
|
+
routes: publicBuild.routes
|
|
31218
|
+
};
|
|
31219
|
+
const manifestPath = path.join(outputDirectory, "manifest.json");
|
|
31220
|
+
yield* fileSystem.writeFileString(manifestPath, `${encodeCanonical(ClientManifest, manifest)}\n`);
|
|
31100
31221
|
return {
|
|
31101
31222
|
assets: output.length + (icon === void 0 ? 0 : 1) + publicBuild.files,
|
|
31102
|
-
|
|
31223
|
+
manifestPath,
|
|
31103
31224
|
shellPath
|
|
31104
31225
|
};
|
|
31105
31226
|
});
|
|
@@ -31495,7 +31616,7 @@ const buildDeploymentArtifact = Effect.fn("Deploy.buildArtifact")(function* (app
|
|
|
31495
31616
|
const client = yield* buildClient(appDirectory, clientOutput);
|
|
31496
31617
|
const server = yield* buildServer(appDirectory, serverOutput, codegen.functionModules);
|
|
31497
31618
|
const payloadFiles = yield* readArtifactDirectory(stagingDirectory);
|
|
31498
|
-
const inventory = yield* makeDeploymentInventory(payloadFiles
|
|
31619
|
+
const inventory = yield* makeDeploymentInventory(payloadFiles);
|
|
31499
31620
|
yield* fileSystem.writeFile(path.join(stagingDirectory, deploymentInventoryPath), inventory.bytes);
|
|
31500
31621
|
const artifactFiles = yield* readArtifactDirectory(stagingDirectory);
|
|
31501
31622
|
yield* validateDeploymentArtifact(artifactFiles);
|
|
@@ -31869,6 +31990,113 @@ const sqliteCauseWithErrno = (cause) => {
|
|
|
31869
31990
|
return Object.assign(cause, { errno: errcode });
|
|
31870
31991
|
};
|
|
31871
31992
|
//#endregion
|
|
31993
|
+
//#region ../contracts/dist/runtime/index.js
|
|
31994
|
+
const IndexIdentity = Schema.String.pipe(Schema.brand("ignotum/runtime/IndexIdentity"));
|
|
31995
|
+
const EncodedIndexKey = Schema.String.check(Schema.isPattern(/^(?:[0-9a-f]{2})*$/)).pipe(Schema.brand("ignotum/runtime/EncodedIndexKey"));
|
|
31996
|
+
const IndexScalarValue = Schema.Union([
|
|
31997
|
+
Schema.Boolean,
|
|
31998
|
+
Schema.Finite,
|
|
31999
|
+
Schema.String
|
|
32000
|
+
]);
|
|
32001
|
+
const scalarValueKind = (value) => Schema.is(Schema.Boolean)(value) ? "boolean" : Schema.is(Schema.Finite)(value) ? "number" : "string";
|
|
32002
|
+
const encodeString = (value) => {
|
|
32003
|
+
const bytes = [];
|
|
32004
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
32005
|
+
const unit = value.charCodeAt(index);
|
|
32006
|
+
bytes.push(1, unit >>> 8, unit & 255);
|
|
32007
|
+
}
|
|
32008
|
+
bytes.push(0);
|
|
32009
|
+
return Array.from(bytes);
|
|
32010
|
+
};
|
|
32011
|
+
const encodeNumber = (input) => {
|
|
32012
|
+
const value = Object.is(input, -0) ? 0 : input;
|
|
32013
|
+
if (!Number.isFinite(value)) throw new Error("Index numbers must be finite.");
|
|
32014
|
+
const bytes = /* @__PURE__ */ new Uint8Array(8);
|
|
32015
|
+
new DataView(bytes.buffer).setFloat64(0, value, false);
|
|
32016
|
+
if ((bytes[0] ?? 0) >= 128) for (let index = 0; index < bytes.length; index += 1) bytes[index] = 255 - bytes[index];
|
|
32017
|
+
else bytes[0] = (bytes[0] ?? 0) ^ 128;
|
|
32018
|
+
return Array.from(bytes);
|
|
32019
|
+
};
|
|
32020
|
+
const scalarKind = (descriptor) => {
|
|
32021
|
+
switch (descriptor.type) {
|
|
32022
|
+
case "boolean": return "boolean";
|
|
32023
|
+
case "date":
|
|
32024
|
+
case "integer":
|
|
32025
|
+
case "number": return "number";
|
|
32026
|
+
case "id":
|
|
32027
|
+
case "string": return "string";
|
|
32028
|
+
case "literal": return scalarValueKind(descriptor.value);
|
|
32029
|
+
case "literals": {
|
|
32030
|
+
const first = descriptor.values[0];
|
|
32031
|
+
if (first === void 0) throw new Error("Indexed literal sets cannot be empty.");
|
|
32032
|
+
const kind = scalarValueKind(first);
|
|
32033
|
+
if (descriptor.values.some((value) => scalarValueKind(value) !== kind)) throw new Error("Indexed literal sets cannot mix value types.");
|
|
32034
|
+
return kind;
|
|
32035
|
+
}
|
|
32036
|
+
default: throw new Error(`Values of type '${descriptor.type}' cannot be indexed.`);
|
|
32037
|
+
}
|
|
32038
|
+
};
|
|
32039
|
+
const encodeScalar = (descriptor, value) => {
|
|
32040
|
+
switch (scalarKind(descriptor)) {
|
|
32041
|
+
case "boolean":
|
|
32042
|
+
if (!Schema.is(Schema.Boolean)(value)) throw new Error("Expected an indexed boolean value.");
|
|
32043
|
+
return [value ? 1 : 0];
|
|
32044
|
+
case "number":
|
|
32045
|
+
if (!Schema.is(Schema.Finite)(value)) throw new Error("Expected an indexed number value.");
|
|
32046
|
+
return encodeNumber(value);
|
|
32047
|
+
case "string":
|
|
32048
|
+
if (!Schema.is(Schema.String)(value)) throw new Error("Expected an indexed string value.");
|
|
32049
|
+
return encodeString(value);
|
|
32050
|
+
}
|
|
32051
|
+
};
|
|
32052
|
+
const encodeBytes = (bytes) => EncodedIndexKey.make(bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(""));
|
|
32053
|
+
const encodeIndexPrefix = (descriptors, values) => {
|
|
32054
|
+
if (descriptors.length !== values.length) throw new Error("Index descriptors and values must have the same length.");
|
|
32055
|
+
const bytes = descriptors.flatMap((descriptor, index) => encodeScalar(descriptor, values[index]));
|
|
32056
|
+
if (bytes.length > schemaIndexLimits.keyBytes) throw new Error(`Index keys cannot exceed ${schemaIndexLimits.keyBytes} bytes.`);
|
|
32057
|
+
return encodeBytes(bytes);
|
|
32058
|
+
};
|
|
32059
|
+
const encodeIndexKey = (descriptors, values, createdAt, id) => {
|
|
32060
|
+
const bytes = [
|
|
32061
|
+
...descriptors.flatMap((descriptor, index) => encodeScalar(descriptor, values[index])),
|
|
32062
|
+
...encodeNumber(createdAt),
|
|
32063
|
+
...encodeString(id)
|
|
32064
|
+
];
|
|
32065
|
+
if (bytes.length > schemaIndexLimits.keyBytes) throw new Error(`Index keys cannot exceed ${schemaIndexLimits.keyBytes} bytes.`);
|
|
32066
|
+
return encodeBytes(bytes);
|
|
32067
|
+
};
|
|
32068
|
+
const successorIndexPrefix = (prefix) => {
|
|
32069
|
+
const bytes = Array.from({ length: prefix.length / 2 }, (_, index) => Number.parseInt(prefix.slice(index * 2, index * 2 + 2), 16));
|
|
32070
|
+
for (let index = bytes.length - 1; index >= 0; index -= 1) {
|
|
32071
|
+
const byte = bytes[index];
|
|
32072
|
+
if (byte === void 0 || byte === 255) continue;
|
|
32073
|
+
bytes[index] = byte + 1;
|
|
32074
|
+
return encodeBytes(bytes.slice(0, index + 1));
|
|
32075
|
+
}
|
|
32076
|
+
};
|
|
32077
|
+
const encodeIndexRange = (descriptors, equal, lower, upper) => {
|
|
32078
|
+
const equalDescriptors = descriptors.slice(0, equal.length);
|
|
32079
|
+
const prefix = encodeIndexPrefix(equalDescriptors, equal);
|
|
32080
|
+
const rangeDescriptor = descriptors[equal.length];
|
|
32081
|
+
if ((lower !== void 0 || upper !== void 0) && rangeDescriptor === void 0) throw new Error("An index range exceeded the index fields.");
|
|
32082
|
+
const encodedLower = lower === void 0 ? prefix.length === 0 ? void 0 : prefix : EncodedIndexKey.make(`${prefix}${encodeIndexPrefix([rangeDescriptor], [lower.value])}`);
|
|
32083
|
+
const encodedUpper = upper === void 0 ? prefix.length === 0 ? void 0 : successorIndexPrefix(prefix) : EncodedIndexKey.make(`${prefix}${encodeIndexPrefix([rangeDescriptor], [upper.value])}`);
|
|
32084
|
+
const finalLower = encodedLower !== void 0 && lower !== void 0 && !lower.inclusive ? successorIndexPrefix(encodedLower) : encodedLower;
|
|
32085
|
+
const finalUpper = encodedUpper !== void 0 && upper !== void 0 && upper.inclusive ? successorIndexPrefix(encodedUpper) : encodedUpper;
|
|
32086
|
+
if (finalLower === void 0 && finalUpper === void 0) return {};
|
|
32087
|
+
if (finalLower === void 0) return { upper: finalUpper };
|
|
32088
|
+
if (finalUpper === void 0) return { lower: finalLower };
|
|
32089
|
+
return {
|
|
32090
|
+
lower: finalLower,
|
|
32091
|
+
upper: finalUpper
|
|
32092
|
+
};
|
|
32093
|
+
};
|
|
32094
|
+
const indexIdentity = (tableId, name, fields) => IndexIdentity.make(encodeCanonicalJson([
|
|
32095
|
+
tableId,
|
|
32096
|
+
name,
|
|
32097
|
+
fields
|
|
32098
|
+
]));
|
|
32099
|
+
//#endregion
|
|
31872
32100
|
//#region ../contracts/dist/runtime/hosted.js
|
|
31873
32101
|
const QueryKey = Schema.String.pipe(Schema.brand("ignotum/hosted/QueryKey"));
|
|
31874
32102
|
const TableDependency = Schema.Struct({
|
|
@@ -31880,9 +32108,32 @@ const DocumentDependency = Schema.Struct({
|
|
|
31880
32108
|
tableId: TableId,
|
|
31881
32109
|
id: GeneratedId
|
|
31882
32110
|
});
|
|
31883
|
-
const
|
|
31884
|
-
|
|
31885
|
-
|
|
32111
|
+
const IndexRangeDependency = Schema.Struct({
|
|
32112
|
+
type: Schema.Literal("IndexRange"),
|
|
32113
|
+
tableId: TableId,
|
|
32114
|
+
index: IndexIdentity,
|
|
32115
|
+
lower: Schema.optional(EncodedIndexKey),
|
|
32116
|
+
upper: Schema.optional(EncodedIndexKey)
|
|
32117
|
+
});
|
|
32118
|
+
const IndexPointInvalidation = Schema.Struct({
|
|
32119
|
+
type: Schema.Literal("IndexPoint"),
|
|
32120
|
+
tableId: TableId,
|
|
32121
|
+
index: IndexIdentity,
|
|
32122
|
+
key: EncodedIndexKey
|
|
32123
|
+
});
|
|
32124
|
+
const ReadDependency = Schema.Union([
|
|
32125
|
+
TableDependency,
|
|
32126
|
+
DocumentDependency,
|
|
32127
|
+
IndexRangeDependency
|
|
32128
|
+
]);
|
|
32129
|
+
const WriteInvalidation = Schema.Union([
|
|
32130
|
+
TableDependency,
|
|
32131
|
+
DocumentDependency,
|
|
32132
|
+
IndexPointInvalidation
|
|
32133
|
+
]);
|
|
32134
|
+
Schema.Union([ReadDependency, IndexPointInvalidation]);
|
|
32135
|
+
const DependencySet = Schema.Array(ReadDependency);
|
|
32136
|
+
const InvalidationSet = Schema.Array(WriteInvalidation);
|
|
31886
32137
|
const RuntimeQueryResult = Schema.Struct({
|
|
31887
32138
|
type: Schema.Literal("Query"),
|
|
31888
32139
|
result: WireResult,
|
|
@@ -31954,10 +32205,19 @@ Schema.Union([
|
|
|
31954
32205
|
RuntimeInvocationRejected,
|
|
31955
32206
|
RuntimeInvocationUnavailable
|
|
31956
32207
|
]);
|
|
31957
|
-
const dependencyKey = (dependency) => dependency.type === "Table" ? encodeCanonicalJson([dependency.type, dependency.tableId]) : encodeCanonicalJson([
|
|
32208
|
+
const dependencyKey = (dependency) => dependency.type === "Table" ? encodeCanonicalJson([dependency.type, dependency.tableId]) : dependency.type === "Document" ? encodeCanonicalJson([
|
|
31958
32209
|
dependency.type,
|
|
31959
32210
|
dependency.tableId,
|
|
31960
32211
|
dependency.id
|
|
32212
|
+
]) : dependency.type === "IndexPoint" ? encodeCanonicalJson([
|
|
32213
|
+
dependency.type,
|
|
32214
|
+
dependency.index,
|
|
32215
|
+
dependency.key
|
|
32216
|
+
]) : encodeCanonicalJson([
|
|
32217
|
+
dependency.type,
|
|
32218
|
+
dependency.index,
|
|
32219
|
+
dependency.lower ?? null,
|
|
32220
|
+
dependency.upper ?? null
|
|
31961
32221
|
]);
|
|
31962
32222
|
const canonicalQueryKey = (deploymentId, functionAddress, args) => QueryKey.make(encodeCanonicalJson([
|
|
31963
32223
|
deploymentId,
|
|
@@ -31979,6 +32239,19 @@ const documentDependency = (tableId, id) => ({
|
|
|
31979
32239
|
tableId,
|
|
31980
32240
|
id
|
|
31981
32241
|
});
|
|
32242
|
+
const indexRangeDependency = (tableId, index, lower, upper) => ({
|
|
32243
|
+
type: "IndexRange",
|
|
32244
|
+
tableId,
|
|
32245
|
+
index,
|
|
32246
|
+
lower,
|
|
32247
|
+
upper
|
|
32248
|
+
});
|
|
32249
|
+
const indexPointInvalidation = (tableId, index, key) => ({
|
|
32250
|
+
type: "IndexPoint",
|
|
32251
|
+
tableId,
|
|
32252
|
+
index,
|
|
32253
|
+
key
|
|
32254
|
+
});
|
|
31982
32255
|
//#endregion
|
|
31983
32256
|
//#region ../runtime/dist/functions.js
|
|
31984
32257
|
var FunctionRuntime = class extends Context.Service()("@ignotum/runtime/functions/FunctionRuntime") {};
|
|
@@ -31987,6 +32260,8 @@ var FunctionRuntime = class extends Context.Service()("@ignotum/runtime/function
|
|
|
31987
32260
|
const makeDependencyIndex = () => {
|
|
31988
32261
|
const dependenciesByQuery = MutableHashMap.empty();
|
|
31989
32262
|
const queriesByDependency = MutableHashMap.empty();
|
|
32263
|
+
const rangesByQuery = MutableHashMap.empty();
|
|
32264
|
+
const queriesByIndex = MutableHashMap.empty();
|
|
31990
32265
|
const remove = (queryId) => {
|
|
31991
32266
|
const dependencies = Option.getOrUndefined(MutableHashMap.get(dependenciesByQuery, queryId));
|
|
31992
32267
|
if (dependencies === void 0) return;
|
|
@@ -31996,12 +32271,23 @@ const makeDependencyIndex = () => {
|
|
|
31996
32271
|
MutableHashSet.remove(queries, queryId);
|
|
31997
32272
|
if (MutableHashSet.size(queries) === 0) MutableHashMap.remove(queriesByDependency, key);
|
|
31998
32273
|
}
|
|
32274
|
+
const ranges = Option.getOrElse(MutableHashMap.get(rangesByQuery, queryId), () => []);
|
|
32275
|
+
for (const range of ranges) {
|
|
32276
|
+
const queries = Option.getOrUndefined(MutableHashMap.get(queriesByIndex, range.index));
|
|
32277
|
+
if (queries === void 0) continue;
|
|
32278
|
+
MutableHashSet.remove(queries, queryId);
|
|
32279
|
+
if (MutableHashSet.size(queries) === 0) MutableHashMap.remove(queriesByIndex, range.index);
|
|
32280
|
+
}
|
|
32281
|
+
MutableHashMap.remove(rangesByQuery, queryId);
|
|
31999
32282
|
MutableHashMap.remove(dependenciesByQuery, queryId);
|
|
32000
32283
|
};
|
|
32001
32284
|
const record = (queryId, dependencies) => {
|
|
32002
32285
|
remove(queryId);
|
|
32003
|
-
const
|
|
32286
|
+
const exact = dependencies.filter((dependency) => dependency.type !== "IndexRange");
|
|
32287
|
+
const ranges = dependencies.filter((dependency) => dependency.type === "IndexRange");
|
|
32288
|
+
const keys = MutableHashSet.fromIterable(exact.map(dependencyKey));
|
|
32004
32289
|
MutableHashMap.set(dependenciesByQuery, queryId, keys);
|
|
32290
|
+
MutableHashMap.set(rangesByQuery, queryId, ranges);
|
|
32005
32291
|
for (const key of keys) {
|
|
32006
32292
|
const current = Option.getOrElse(MutableHashMap.get(queriesByDependency, key), () => {
|
|
32007
32293
|
const created = MutableHashSet.empty();
|
|
@@ -32010,10 +32296,24 @@ const makeDependencyIndex = () => {
|
|
|
32010
32296
|
});
|
|
32011
32297
|
MutableHashSet.add(current, queryId);
|
|
32012
32298
|
}
|
|
32299
|
+
for (const range of ranges) {
|
|
32300
|
+
const current = Option.getOrElse(MutableHashMap.get(queriesByIndex, range.index), () => {
|
|
32301
|
+
const created = MutableHashSet.empty();
|
|
32302
|
+
MutableHashMap.set(queriesByIndex, range.index, created);
|
|
32303
|
+
return created;
|
|
32304
|
+
});
|
|
32305
|
+
MutableHashSet.add(current, queryId);
|
|
32306
|
+
}
|
|
32013
32307
|
};
|
|
32014
32308
|
const affected = (invalidations) => {
|
|
32015
32309
|
const queries = MutableHashSet.empty();
|
|
32016
32310
|
for (const invalidation of invalidations) {
|
|
32311
|
+
if (invalidation.type === "IndexPoint") {
|
|
32312
|
+
const indexed = Option.getOrUndefined(MutableHashMap.get(queriesByIndex, invalidation.index));
|
|
32313
|
+
if (indexed === void 0) continue;
|
|
32314
|
+
for (const queryId of indexed) if (Option.getOrElse(MutableHashMap.get(rangesByQuery, queryId), () => []).some((range) => range.index === invalidation.index && (range.lower === void 0 || invalidation.key >= range.lower) && (range.upper === void 0 || invalidation.key < range.upper))) MutableHashSet.add(queries, queryId);
|
|
32315
|
+
continue;
|
|
32316
|
+
}
|
|
32017
32317
|
const current = Option.getOrUndefined(MutableHashMap.get(queriesByDependency, dependencyKey(invalidation)));
|
|
32018
32318
|
if (current !== void 0) for (const queryId of current) MutableHashSet.add(queries, queryId);
|
|
32019
32319
|
}
|
|
@@ -32030,8 +32330,15 @@ var QueryInvalidation = class extends Context.Service()("@ignotum/runtime/sync/Q
|
|
|
32030
32330
|
//#region ../contracts/dist/runtime/transport.js
|
|
32031
32331
|
const runtimeInvocationPath = "/v1/invoke";
|
|
32032
32332
|
const runtimeRevisionPath = "/v1/revision";
|
|
32333
|
+
const runtimeIndexPreparePath = "/v1/indexes/prepare";
|
|
32334
|
+
const runtimeIndexCommitPath = "/v1/indexes/commit";
|
|
32033
32335
|
const appSyncPath = `/_ignotum/v1/sync`;
|
|
32034
|
-
const RuntimeRequestPath = Schema.Literals([
|
|
32336
|
+
const RuntimeRequestPath = Schema.Literals([
|
|
32337
|
+
runtimeInvocationPath,
|
|
32338
|
+
runtimeRevisionPath,
|
|
32339
|
+
runtimeIndexPreparePath,
|
|
32340
|
+
runtimeIndexCommitPath
|
|
32341
|
+
]);
|
|
32035
32342
|
const RuntimeRequestTimestamp = Schema.FiniteFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ignotum/runtime/RequestTimestamp"));
|
|
32036
32343
|
const RuntimeRequestSignature = Sha256.pipe(Schema.brand("ignotum/runtime/RequestSignature"));
|
|
32037
32344
|
Schema.Struct({
|
|
@@ -32050,6 +32357,11 @@ Schema.Struct({
|
|
|
32050
32357
|
deploymentId: Schema.optional(DeploymentId),
|
|
32051
32358
|
bodySha256: Sha256
|
|
32052
32359
|
});
|
|
32360
|
+
Schema.Struct({
|
|
32361
|
+
appId: AppId,
|
|
32362
|
+
deploymentId: DeploymentId
|
|
32363
|
+
});
|
|
32364
|
+
Schema.Struct({ completed: Schema.Literal(true) });
|
|
32053
32365
|
const RuntimeErrorCode = Schema.Literals([
|
|
32054
32366
|
"InvalidRequest",
|
|
32055
32367
|
"InvalidSignature",
|
|
@@ -32110,7 +32422,38 @@ const initialDevelopmentSchema = Effect.gen(function* () {
|
|
|
32110
32422
|
) STRICT
|
|
32111
32423
|
`;
|
|
32112
32424
|
});
|
|
32113
|
-
const
|
|
32425
|
+
const addDevelopmentIndexes = Effect.gen(function* () {
|
|
32426
|
+
const sql = yield* Client.SqlClient;
|
|
32427
|
+
yield* sql`
|
|
32428
|
+
CREATE TABLE indexes (
|
|
32429
|
+
id INTEGER PRIMARY KEY NOT NULL,
|
|
32430
|
+
tableId TEXT NOT NULL REFERENCES tables(id) ON DELETE CASCADE,
|
|
32431
|
+
identity TEXT NOT NULL UNIQUE,
|
|
32432
|
+
name TEXT NOT NULL,
|
|
32433
|
+
fields TEXT NOT NULL CHECK (json_valid(fields))
|
|
32434
|
+
) STRICT
|
|
32435
|
+
`;
|
|
32436
|
+
yield* sql`
|
|
32437
|
+
CREATE TABLE indexEntries (
|
|
32438
|
+
indexId INTEGER NOT NULL REFERENCES indexes(id) ON DELETE CASCADE,
|
|
32439
|
+
tableId TEXT NOT NULL,
|
|
32440
|
+
documentId TEXT NOT NULL,
|
|
32441
|
+
key TEXT NOT NULL,
|
|
32442
|
+
PRIMARY KEY (indexId, documentId),
|
|
32443
|
+
FOREIGN KEY (tableId, documentId)
|
|
32444
|
+
REFERENCES documents(tableId, id)
|
|
32445
|
+
ON DELETE CASCADE
|
|
32446
|
+
) STRICT
|
|
32447
|
+
`;
|
|
32448
|
+
yield* sql`
|
|
32449
|
+
CREATE UNIQUE INDEX index_entries_by_key
|
|
32450
|
+
ON indexEntries (indexId, key)
|
|
32451
|
+
`;
|
|
32452
|
+
});
|
|
32453
|
+
const developmentMigrationLoader = Migrator.fromRecord({
|
|
32454
|
+
"0001_initial_development_schema": initialDevelopmentSchema,
|
|
32455
|
+
"0002_application_indexes": addDevelopmentIndexes
|
|
32456
|
+
});
|
|
32114
32457
|
var DevelopmentDatabase = class DevelopmentDatabase extends Context.Service()("ignotum/dev-runtime/migrations/DevelopmentDatabase") {
|
|
32115
32458
|
static layer = Layer.effect(DevelopmentDatabase, Migrator.make({})({
|
|
32116
32459
|
loader: developmentMigrationLoader,
|
|
@@ -32125,6 +32468,16 @@ const StoredDocument = Schema.Struct({
|
|
|
32125
32468
|
updatedAt: Schema.Int,
|
|
32126
32469
|
fields: Schema.String
|
|
32127
32470
|
});
|
|
32471
|
+
const IndexedStoredDocument = Schema.Struct({
|
|
32472
|
+
...StoredDocument.fields,
|
|
32473
|
+
key: Schema.String
|
|
32474
|
+
});
|
|
32475
|
+
const StoredIndex = Schema.Struct({
|
|
32476
|
+
id: Schema.Natural,
|
|
32477
|
+
identity: Schema.String,
|
|
32478
|
+
name: Schema.String,
|
|
32479
|
+
fields: Schema.String
|
|
32480
|
+
});
|
|
32128
32481
|
const StoredTable = Schema.Struct({
|
|
32129
32482
|
id: TableId,
|
|
32130
32483
|
name: Schema.String
|
|
@@ -32197,6 +32550,18 @@ const toRuntimeDocument = Effect.fn("LocalDatabase.toRuntimeDocument")(function*
|
|
|
32197
32550
|
};
|
|
32198
32551
|
});
|
|
32199
32552
|
const nextUpdatedAt = (now, previous) => Math.max(DateTime.toEpochMillis(now), previous + 1);
|
|
32553
|
+
const decodeIndexScalar = Schema.decodeUnknownSync(IndexScalarValue);
|
|
32554
|
+
const indexDescriptors = (table, fields) => fields.map((field) => {
|
|
32555
|
+
const schema = table.fields[field];
|
|
32556
|
+
const descriptor = schema === void 0 ? void 0 : getValueDescriptor(schema);
|
|
32557
|
+
if (descriptor === void 0) throw new Error(`Index field '${field}' has no descriptor.`);
|
|
32558
|
+
return descriptor;
|
|
32559
|
+
});
|
|
32560
|
+
const storedIndexKey = (index, row) => {
|
|
32561
|
+
const fields = Schema.decodeSync(Schema.fromJsonString(Schema.JsonObject))(row.fields);
|
|
32562
|
+
const values = index.definition.fields.map((field) => decodeIndexScalar(fields[field]));
|
|
32563
|
+
return encodeIndexKey(index.descriptors, values, row.createdAt, row.id);
|
|
32564
|
+
};
|
|
32200
32565
|
var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-runtime/database/LocalDatabase") {
|
|
32201
32566
|
static layer = Layer.effect(LocalDatabase, Effect.gen(function* () {
|
|
32202
32567
|
const ids = yield* IdGenerator;
|
|
@@ -32240,6 +32605,31 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32240
32605
|
FROM documents
|
|
32241
32606
|
WHERE tableId = ${tableId}
|
|
32242
32607
|
ORDER BY createdAt ASC, id ASC
|
|
32608
|
+
`
|
|
32609
|
+
});
|
|
32610
|
+
const listIndexes = SqlSchema.findAll({
|
|
32611
|
+
Request: TableLookup,
|
|
32612
|
+
Result: StoredIndex,
|
|
32613
|
+
execute: ({ tableId }) => sql`
|
|
32614
|
+
SELECT id, identity, name, fields
|
|
32615
|
+
FROM indexes
|
|
32616
|
+
WHERE tableId = ${tableId}
|
|
32617
|
+
ORDER BY id ASC
|
|
32618
|
+
`
|
|
32619
|
+
});
|
|
32620
|
+
const insertIndex = SqlSchema.findOne({
|
|
32621
|
+
Request: Schema.Struct({
|
|
32622
|
+
tableId: TableId,
|
|
32623
|
+
identity: Schema.String,
|
|
32624
|
+
name: Schema.String,
|
|
32625
|
+
fields: Schema.String
|
|
32626
|
+
}),
|
|
32627
|
+
Result: StoredIndex,
|
|
32628
|
+
execute: ({ tableId, identity, name, fields }) => sql`
|
|
32629
|
+
INSERT INTO indexes (tableId, identity, name, fields)
|
|
32630
|
+
VALUES (${tableId}, ${identity}, ${name}, ${fields})
|
|
32631
|
+
ON CONFLICT (identity) DO UPDATE SET identity = excluded.identity
|
|
32632
|
+
RETURNING id, identity, name, fields
|
|
32243
32633
|
`
|
|
32244
32634
|
});
|
|
32245
32635
|
const currentRevision = SqlSchema.findOne({
|
|
@@ -32273,6 +32663,64 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32273
32663
|
definition
|
|
32274
32664
|
};
|
|
32275
32665
|
});
|
|
32666
|
+
const resolvedIndexes = Effect.fn("LocalDatabase.resolvedIndexes")(function* (table) {
|
|
32667
|
+
const stored = yield* listIndexes({ tableId: table.id });
|
|
32668
|
+
return Object.values(table.definition.indexes).map((definition) => {
|
|
32669
|
+
const identity = indexIdentity(table.id, definition.name, definition.fields);
|
|
32670
|
+
const row = stored.find((candidate) => candidate.identity === identity);
|
|
32671
|
+
if (row === void 0) throw new Error(`Index '${definition.name}' is not prepared.`);
|
|
32672
|
+
return {
|
|
32673
|
+
...row,
|
|
32674
|
+
definition,
|
|
32675
|
+
descriptors: indexDescriptors(table.definition, definition.fields)
|
|
32676
|
+
};
|
|
32677
|
+
});
|
|
32678
|
+
});
|
|
32679
|
+
const prepareIndexes = Effect.fn("LocalDatabase.prepareIndexes")(function* (schema) {
|
|
32680
|
+
for (const [tableName, definition] of Object.entries(schema[SchemaDefinitionTypeId])) {
|
|
32681
|
+
const table = yield* resolveTable(schema, tableName);
|
|
32682
|
+
const stored = yield* listIndexes({ tableId: table.id });
|
|
32683
|
+
const expected = /* @__PURE__ */ new Set();
|
|
32684
|
+
for (const index of Object.values(definition.indexes)) {
|
|
32685
|
+
const identity = indexIdentity(table.id, index.name, index.fields);
|
|
32686
|
+
expected.add(identity);
|
|
32687
|
+
if (stored.some((candidate) => candidate.identity === identity)) continue;
|
|
32688
|
+
const fields = JSON.stringify(index.fields);
|
|
32689
|
+
const resolved = {
|
|
32690
|
+
...yield* insertIndex({
|
|
32691
|
+
tableId: table.id,
|
|
32692
|
+
identity,
|
|
32693
|
+
name: index.name,
|
|
32694
|
+
fields
|
|
32695
|
+
}).pipe(Effect.catchTags({ NoSuchElementError: Effect.die })),
|
|
32696
|
+
definition: index,
|
|
32697
|
+
descriptors: indexDescriptors(definition, index.fields)
|
|
32698
|
+
};
|
|
32699
|
+
const documents = yield* collectStored({ tableId: table.id });
|
|
32700
|
+
yield* Effect.forEach(documents, (document) => sql`
|
|
32701
|
+
INSERT INTO indexEntries (indexId, tableId, documentId, key)
|
|
32702
|
+
VALUES (${resolved.id}, ${table.id}, ${document.id}, ${storedIndexKey(resolved, document)})
|
|
32703
|
+
`);
|
|
32704
|
+
}
|
|
32705
|
+
yield* Effect.forEach(stored.filter((index) => !expected.has(index.identity)), (index) => sql`DELETE FROM indexes WHERE id = ${index.id}`);
|
|
32706
|
+
}
|
|
32707
|
+
});
|
|
32708
|
+
const indexKeys = Effect.fn("LocalDatabase.indexKeys")(function* (table, row) {
|
|
32709
|
+
return (yield* resolvedIndexes(table)).map((index) => ({
|
|
32710
|
+
index,
|
|
32711
|
+
key: storedIndexKey(index, row),
|
|
32712
|
+
identity: indexIdentity(table.id, index.name, index.definition.fields)
|
|
32713
|
+
}));
|
|
32714
|
+
});
|
|
32715
|
+
const writeIndexEntries = Effect.fn("LocalDatabase.writeIndexEntries")(function* (table, row) {
|
|
32716
|
+
const keys = yield* indexKeys(table, row);
|
|
32717
|
+
yield* Effect.forEach(keys, ({ index, key }) => sql`
|
|
32718
|
+
INSERT INTO indexEntries (indexId, tableId, documentId, key)
|
|
32719
|
+
VALUES (${index.id}, ${table.id}, ${row.id}, ${key})
|
|
32720
|
+
ON CONFLICT (indexId, documentId) DO UPDATE SET key = excluded.key
|
|
32721
|
+
`);
|
|
32722
|
+
return keys;
|
|
32723
|
+
});
|
|
32276
32724
|
const find = Effect.fn("LocalDatabase.find")(function* (schema, tableName, id, dependencies) {
|
|
32277
32725
|
const table = yield* resolveTable(schema, tableName);
|
|
32278
32726
|
const documentId = GeneratedId.make(id);
|
|
@@ -32284,11 +32732,100 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32284
32732
|
if (Option.isNone(row)) return void 0;
|
|
32285
32733
|
return yield* toRuntimeDocument(table.definition, tableName, row.value, "get");
|
|
32286
32734
|
});
|
|
32287
|
-
const
|
|
32735
|
+
const queryDocuments = Effect.fn("LocalDatabase.queryDocuments")(function* (schema, tableName, selectedIndex, bounds, order, limit, pagination, dependencies) {
|
|
32288
32736
|
const table = yield* resolveTable(schema, tableName);
|
|
32289
|
-
|
|
32290
|
-
|
|
32291
|
-
|
|
32737
|
+
if (selectedIndex === void 0) {
|
|
32738
|
+
dependencies?.record(tableDependency(table.id));
|
|
32739
|
+
const queryIdentity = paginationQueryIdentity(table.id, null, void 0, void 0, order);
|
|
32740
|
+
const decodedCursor = pagination?.cursor === null || pagination === void 0 ? void 0 : decodePaginationCursor(pagination.cursor);
|
|
32741
|
+
if (decodedCursor !== void 0 && decodedCursor.query !== queryIdentity) throw new Error("A pagination cursor belongs to another query.");
|
|
32742
|
+
const cursor = decodedCursor?.position;
|
|
32743
|
+
if (cursor !== void 0 && cursor.type !== "Table") throw new Error("A pagination cursor has the wrong position type.");
|
|
32744
|
+
const clauses = ["tableId = ?"];
|
|
32745
|
+
const parameters = [table.id];
|
|
32746
|
+
if (cursor !== void 0) {
|
|
32747
|
+
const operator = order === "Asc" ? ">" : "<";
|
|
32748
|
+
clauses.push(`(createdAt ${operator} ? OR (createdAt = ? AND id ${operator} ?))`);
|
|
32749
|
+
parameters.push(cursor.createdAt, cursor.createdAt, cursor.id);
|
|
32750
|
+
}
|
|
32751
|
+
const storageLimit = pagination === void 0 ? limit : pagination.pageSize + 1;
|
|
32752
|
+
if (storageLimit !== void 0) parameters.push(storageLimit);
|
|
32753
|
+
const rows = yield* sql.unsafe(`SELECT id, createdAt, updatedAt, fields
|
|
32754
|
+
FROM documents
|
|
32755
|
+
WHERE ${clauses.join(" AND ")}
|
|
32756
|
+
ORDER BY createdAt ${order === "Asc" ? "ASC" : "DESC"}, id ${order === "Asc" ? "ASC" : "DESC"}${storageLimit === void 0 ? "" : " LIMIT ?"}`, parameters);
|
|
32757
|
+
const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(StoredDocument))(rows);
|
|
32758
|
+
const hasMore = pagination !== void 0 && decoded.length > pagination.pageSize;
|
|
32759
|
+
const pageRows = pagination === void 0 ? decoded : decoded.slice(0, pagination.pageSize);
|
|
32760
|
+
const documents = yield* Effect.forEach(pageRows, (row) => toRuntimeDocument(table.definition, tableName, row, "collect"));
|
|
32761
|
+
const last = pageRows.at(-1);
|
|
32762
|
+
return {
|
|
32763
|
+
documents,
|
|
32764
|
+
nextCursor: !hasMore || last === void 0 ? null : encodePaginationCursor(queryIdentity, {
|
|
32765
|
+
type: "Table",
|
|
32766
|
+
createdAt: last.createdAt,
|
|
32767
|
+
id: last.id
|
|
32768
|
+
})
|
|
32769
|
+
};
|
|
32770
|
+
}
|
|
32771
|
+
const index = (yield* resolvedIndexes(table)).find((candidate) => candidate.name === selectedIndex);
|
|
32772
|
+
if (index === void 0) throw new Error(`Unknown index '${selectedIndex}'.`);
|
|
32773
|
+
const identity = indexIdentity(table.id, index.name, index.definition.fields);
|
|
32774
|
+
const queryIdentity = paginationQueryIdentity(table.id, identity, bounds.lower, bounds.upper, order);
|
|
32775
|
+
const decodedCursor = pagination?.cursor === null || pagination === void 0 ? void 0 : decodePaginationCursor(pagination.cursor);
|
|
32776
|
+
if (decodedCursor !== void 0 && decodedCursor.query !== queryIdentity) throw new Error("A pagination cursor belongs to another query.");
|
|
32777
|
+
const cursor = decodedCursor?.position;
|
|
32778
|
+
if (cursor !== void 0 && cursor.type !== "Index") throw new Error("A pagination cursor has the wrong position type.");
|
|
32779
|
+
const cursorLower = cursor !== void 0 && order === "Asc" ? successorIndexPrefix(cursor.key) : void 0;
|
|
32780
|
+
const cursorUpper = cursor !== void 0 && order === "Desc" ? cursor.key : void 0;
|
|
32781
|
+
const effectiveBounds = {
|
|
32782
|
+
lower: bounds.lower === void 0 ? cursorLower : cursorLower === void 0 || bounds.lower > cursorLower ? bounds.lower : cursorLower,
|
|
32783
|
+
upper: bounds.upper === void 0 ? cursorUpper : cursorUpper === void 0 || bounds.upper < cursorUpper ? bounds.upper : cursorUpper
|
|
32784
|
+
};
|
|
32785
|
+
const clauses = ["entries.indexId = ?"];
|
|
32786
|
+
const parameters = [index.id];
|
|
32787
|
+
if (effectiveBounds.lower !== void 0) {
|
|
32788
|
+
clauses.push("entries.key >= ?");
|
|
32789
|
+
parameters.push(effectiveBounds.lower);
|
|
32790
|
+
}
|
|
32791
|
+
if (effectiveBounds.upper !== void 0) {
|
|
32792
|
+
clauses.push("entries.key < ?");
|
|
32793
|
+
parameters.push(effectiveBounds.upper);
|
|
32794
|
+
}
|
|
32795
|
+
const storageLimit = pagination === void 0 ? limit : pagination.pageSize + 1;
|
|
32796
|
+
if (storageLimit !== void 0) parameters.push(storageLimit);
|
|
32797
|
+
const rows = yield* sql.unsafe(`SELECT documents.id, documents.createdAt, documents.updatedAt, documents.fields,
|
|
32798
|
+
entries.key
|
|
32799
|
+
FROM indexEntries AS entries
|
|
32800
|
+
JOIN documents
|
|
32801
|
+
ON documents.tableId = entries.tableId
|
|
32802
|
+
AND documents.id = entries.documentId
|
|
32803
|
+
WHERE ${clauses.join(" AND ")}
|
|
32804
|
+
ORDER BY entries.key ${order === "Asc" ? "ASC" : "DESC"}${storageLimit === void 0 ? "" : " LIMIT ?"}`, parameters);
|
|
32805
|
+
const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(IndexedStoredDocument))(rows);
|
|
32806
|
+
const hasMore = pagination !== void 0 && decoded.length > pagination.pageSize;
|
|
32807
|
+
const pageRows = pagination === void 0 ? decoded : decoded.slice(0, pagination.pageSize);
|
|
32808
|
+
let dependencyBounds = effectiveBounds;
|
|
32809
|
+
if (pagination === void 0 ? limit !== void 0 && limit > 0 && decoded.length === limit : hasMore) {
|
|
32810
|
+
const last = pageRows.at(-1);
|
|
32811
|
+
if (last !== void 0) dependencyBounds = order === "Asc" ? {
|
|
32812
|
+
...effectiveBounds,
|
|
32813
|
+
upper: successorIndexPrefix(EncodedIndexKey.make(last.key))
|
|
32814
|
+
} : {
|
|
32815
|
+
...effectiveBounds,
|
|
32816
|
+
lower: EncodedIndexKey.make(last.key)
|
|
32817
|
+
};
|
|
32818
|
+
}
|
|
32819
|
+
dependencies?.record(indexRangeDependency(table.id, identity, dependencyBounds.lower, dependencyBounds.upper));
|
|
32820
|
+
const documents = yield* Effect.forEach(pageRows, (row) => toRuntimeDocument(table.definition, tableName, row, "collect"));
|
|
32821
|
+
const last = pageRows.at(-1);
|
|
32822
|
+
return {
|
|
32823
|
+
documents,
|
|
32824
|
+
nextCursor: !hasMore || last === void 0 ? null : encodePaginationCursor(queryIdentity, {
|
|
32825
|
+
type: "Index",
|
|
32826
|
+
key: EncodedIndexKey.make(last.key)
|
|
32827
|
+
})
|
|
32828
|
+
};
|
|
32292
32829
|
});
|
|
32293
32830
|
const insert = Effect.fn("LocalDatabase.insert")(function* (schema, tableName, value, invalidations) {
|
|
32294
32831
|
const table = yield* resolveTable(schema, tableName);
|
|
@@ -32299,14 +32836,25 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32299
32836
|
INSERT INTO documents (id, tableId, createdAt, updatedAt, fields)
|
|
32300
32837
|
VALUES (${id}, ${table.id}, ${now}, ${now}, ${fields})
|
|
32301
32838
|
`;
|
|
32302
|
-
|
|
32839
|
+
const keys = yield* writeIndexEntries(table, {
|
|
32840
|
+
id,
|
|
32841
|
+
createdAt: now,
|
|
32842
|
+
updatedAt: now,
|
|
32843
|
+
fields
|
|
32844
|
+
});
|
|
32845
|
+
invalidations.record(tableDependency(table.id), documentDependency(table.id, id), ...keys.map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
|
|
32303
32846
|
return id;
|
|
32304
32847
|
});
|
|
32305
32848
|
const deleteDocument = Effect.fn("LocalDatabase.delete")(function* (schema, tableName, id, invalidations) {
|
|
32306
32849
|
const table = yield* resolveTable(schema, tableName);
|
|
32307
32850
|
const documentId = GeneratedId.make(id);
|
|
32851
|
+
const stored = yield* findStored({
|
|
32852
|
+
id,
|
|
32853
|
+
tableId: table.id
|
|
32854
|
+
});
|
|
32855
|
+
const keys = Option.isSome(stored) ? yield* indexKeys(table, stored.value) : [];
|
|
32308
32856
|
yield* sql`DELETE FROM documents WHERE id = ${id} AND tableId = ${table.id}`;
|
|
32309
|
-
invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId));
|
|
32857
|
+
invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId), ...keys.map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
|
|
32310
32858
|
});
|
|
32311
32859
|
const patch = Effect.fn("LocalDatabase.patch")(function* (schema, tableName, id, value, invalidations) {
|
|
32312
32860
|
const table = yield* resolveTable(schema, tableName);
|
|
@@ -32315,6 +32863,8 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32315
32863
|
id,
|
|
32316
32864
|
tableId: table.id
|
|
32317
32865
|
});
|
|
32866
|
+
const oldKeys = Option.isSome(stored) ? yield* indexKeys(table, stored.value) : [];
|
|
32867
|
+
let newKeys = oldKeys.slice(0, 0);
|
|
32318
32868
|
if (Option.isSome(stored)) {
|
|
32319
32869
|
const current = yield* decodeFields(table.definition, tableName, stored.value, "patch");
|
|
32320
32870
|
const fields = yield* encodeFields(table.definition, tableName, {
|
|
@@ -32327,8 +32877,13 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32327
32877
|
SET fields = ${fields}, updatedAt = ${updatedAt}
|
|
32328
32878
|
WHERE id = ${id} AND tableId = ${table.id}
|
|
32329
32879
|
`;
|
|
32880
|
+
newKeys = yield* writeIndexEntries(table, {
|
|
32881
|
+
...stored.value,
|
|
32882
|
+
fields,
|
|
32883
|
+
updatedAt
|
|
32884
|
+
});
|
|
32330
32885
|
}
|
|
32331
|
-
invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId));
|
|
32886
|
+
invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId), ...[...oldKeys, ...newKeys].map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
|
|
32332
32887
|
});
|
|
32333
32888
|
const replace = Effect.fn("LocalDatabase.replace")(function* (schema, tableName, id, value, invalidations) {
|
|
32334
32889
|
const table = yield* resolveTable(schema, tableName);
|
|
@@ -32337,6 +32892,8 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32337
32892
|
id,
|
|
32338
32893
|
tableId: table.id
|
|
32339
32894
|
});
|
|
32895
|
+
const oldKeys = Option.isSome(stored) ? yield* indexKeys(table, stored.value) : [];
|
|
32896
|
+
let newKeys = oldKeys.slice(0, 0);
|
|
32340
32897
|
if (Option.isSome(stored)) {
|
|
32341
32898
|
const fields = yield* encodeFields(table.definition, tableName, value, "replace", id);
|
|
32342
32899
|
const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
|
|
@@ -32345,14 +32902,121 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32345
32902
|
SET fields = ${fields}, updatedAt = ${updatedAt}
|
|
32346
32903
|
WHERE id = ${id} AND tableId = ${table.id}
|
|
32347
32904
|
`;
|
|
32905
|
+
newKeys = yield* writeIndexEntries(table, {
|
|
32906
|
+
...stored.value,
|
|
32907
|
+
fields,
|
|
32908
|
+
updatedAt
|
|
32909
|
+
});
|
|
32348
32910
|
}
|
|
32349
|
-
invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId));
|
|
32350
|
-
});
|
|
32351
|
-
const makeReader = (schema, dependencies) => Object.freeze({
|
|
32352
|
-
find: (tableName, id) => resultFromEffect(find(schema, tableName, id, dependencies).pipe(Effect.orDie)),
|
|
32353
|
-
get: (tableName, id) => resultFromEffect(find(schema, tableName, id, dependencies).pipe(Effect.orDie, Effect.flatMap((document) => document === void 0 ? Effect.fail(documentNotFound(tableName, id)) : Effect.succeed(document)))),
|
|
32354
|
-
query: (tableName) => Object.freeze({ collect: () => resultFromEffect(collect(schema, tableName, dependencies).pipe(Effect.orDie)) })
|
|
32911
|
+
invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId), ...[...oldKeys, ...newKeys].map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
|
|
32355
32912
|
});
|
|
32913
|
+
const makeReader = (schema, dependencies) => {
|
|
32914
|
+
const databaseQuery = (tableName, selectedIndex, selectedBounds = {}, order = "Asc") => {
|
|
32915
|
+
const table = schema[SchemaDefinitionTypeId][tableName];
|
|
32916
|
+
if (table === void 0) throw new Error(`Unknown database table '${tableName}'.`);
|
|
32917
|
+
const run = (limit) => queryDocuments(schema, tableName, selectedIndex, selectedBounds, order, limit, void 0, dependencies).pipe(Effect.map(({ documents }) => documents), Effect.orDie);
|
|
32918
|
+
const runPage = (pagination) => queryDocuments(schema, tableName, selectedIndex, selectedBounds, order, void 0, pagination, dependencies).pipe(Effect.orDie);
|
|
32919
|
+
const validateLimit = (count) => {
|
|
32920
|
+
if (!Number.isInteger(count) || count < 0 || count > 1e3) throw new Error("Query limits must be integers between 0 and 1000.");
|
|
32921
|
+
return count;
|
|
32922
|
+
};
|
|
32923
|
+
return Object.freeze({
|
|
32924
|
+
index: (name, buildRange) => {
|
|
32925
|
+
if (selectedIndex !== void 0) throw new Error("A query can select only one index.");
|
|
32926
|
+
const definition = table.indexes[name];
|
|
32927
|
+
if (definition === void 0) throw new Error(`Unknown index '${name}' on table '${tableName}'.`);
|
|
32928
|
+
if (buildRange === void 0) return databaseQuery(tableName, name, {}, order);
|
|
32929
|
+
const fields = [
|
|
32930
|
+
...definition.fields,
|
|
32931
|
+
"createdAt",
|
|
32932
|
+
"id"
|
|
32933
|
+
];
|
|
32934
|
+
const descriptors = [
|
|
32935
|
+
...indexDescriptors(table, definition.fields),
|
|
32936
|
+
{ type: "date" },
|
|
32937
|
+
{
|
|
32938
|
+
type: "id",
|
|
32939
|
+
table: tableName
|
|
32940
|
+
}
|
|
32941
|
+
];
|
|
32942
|
+
const equal = [];
|
|
32943
|
+
let lower;
|
|
32944
|
+
let upper;
|
|
32945
|
+
let position = 0;
|
|
32946
|
+
const encodeValue = (field, value) => {
|
|
32947
|
+
const codec = field === "createdAt" ? Schema.DateFromMillis : field === "id" ? GeneratedId : table.fields[field];
|
|
32948
|
+
if (codec === void 0) throw new Error(`Unknown index field '${field}'.`);
|
|
32949
|
+
return decodeIndexScalar(Schema.encodeUnknownSync(codec)(value));
|
|
32950
|
+
};
|
|
32951
|
+
const expectField = (field) => {
|
|
32952
|
+
const expected = fields[position];
|
|
32953
|
+
if (field !== expected) throw new Error(`Expected index field '${expected}', received '${field}'.`);
|
|
32954
|
+
};
|
|
32955
|
+
const builder = {
|
|
32956
|
+
eq: (field, value) => {
|
|
32957
|
+
if (lower !== void 0 || upper !== void 0) throw new Error("Equality cannot follow an index bound.");
|
|
32958
|
+
expectField(field);
|
|
32959
|
+
equal.push(encodeValue(field, value));
|
|
32960
|
+
position += 1;
|
|
32961
|
+
return builder;
|
|
32962
|
+
},
|
|
32963
|
+
gt: (field, value) => {
|
|
32964
|
+
expectField(field);
|
|
32965
|
+
if (lower !== void 0) throw new Error("An index range has one lower bound.");
|
|
32966
|
+
lower = {
|
|
32967
|
+
value: encodeValue(field, value),
|
|
32968
|
+
inclusive: false
|
|
32969
|
+
};
|
|
32970
|
+
return builder;
|
|
32971
|
+
},
|
|
32972
|
+
gte: (field, value) => {
|
|
32973
|
+
expectField(field);
|
|
32974
|
+
if (lower !== void 0) throw new Error("An index range has one lower bound.");
|
|
32975
|
+
lower = {
|
|
32976
|
+
value: encodeValue(field, value),
|
|
32977
|
+
inclusive: true
|
|
32978
|
+
};
|
|
32979
|
+
return builder;
|
|
32980
|
+
},
|
|
32981
|
+
lt: (field, value) => {
|
|
32982
|
+
expectField(field);
|
|
32983
|
+
if (upper !== void 0) throw new Error("An index range has one upper bound.");
|
|
32984
|
+
upper = {
|
|
32985
|
+
value: encodeValue(field, value),
|
|
32986
|
+
inclusive: false
|
|
32987
|
+
};
|
|
32988
|
+
return builder;
|
|
32989
|
+
},
|
|
32990
|
+
lte: (field, value) => {
|
|
32991
|
+
expectField(field);
|
|
32992
|
+
if (upper !== void 0) throw new Error("An index range has one upper bound.");
|
|
32993
|
+
upper = {
|
|
32994
|
+
value: encodeValue(field, value),
|
|
32995
|
+
inclusive: true
|
|
32996
|
+
};
|
|
32997
|
+
return builder;
|
|
32998
|
+
}
|
|
32999
|
+
};
|
|
33000
|
+
buildRange(builder);
|
|
33001
|
+
return databaseQuery(tableName, name, encodeIndexRange(descriptors, equal, lower, upper), order);
|
|
33002
|
+
},
|
|
33003
|
+
order: (direction) => databaseQuery(tableName, selectedIndex, selectedBounds, direction === "asc" ? "Asc" : "Desc"),
|
|
33004
|
+
collect: () => resultFromEffect(run()),
|
|
33005
|
+
take: (count) => resultFromEffect(run(validateLimit(count))),
|
|
33006
|
+
first: () => resultFromEffect(run(1).pipe(Effect.map((documents) => documents[0]))),
|
|
33007
|
+
unique: () => resultFromEffect(run(2).pipe(Effect.flatMap((documents) => documents.length > 1 ? Effect.die(/* @__PURE__ */ new Error("A unique query matched more than one document.")) : Effect.succeed(documents[0])))),
|
|
33008
|
+
paginate: (options) => resultFromEffect(runPage(options).pipe(Effect.map(({ documents, nextCursor }) => ({
|
|
33009
|
+
items: documents,
|
|
33010
|
+
nextCursor
|
|
33011
|
+
}))))
|
|
33012
|
+
});
|
|
33013
|
+
};
|
|
33014
|
+
return Object.freeze({
|
|
33015
|
+
find: (tableName, id) => resultFromEffect(find(schema, tableName, id, dependencies).pipe(Effect.orDie)),
|
|
33016
|
+
get: (tableName, id) => resultFromEffect(find(schema, tableName, id, dependencies).pipe(Effect.orDie, Effect.flatMap((document) => document === void 0 ? Effect.fail(documentNotFound(tableName, id)) : Effect.succeed(document)))),
|
|
33017
|
+
query: (tableName) => databaseQuery(tableName)
|
|
33018
|
+
});
|
|
33019
|
+
};
|
|
32356
33020
|
const makeWriter = (schema, invalidations) => {
|
|
32357
33021
|
const reader = makeReader(schema);
|
|
32358
33022
|
return Object.freeze({
|
|
@@ -32364,6 +33028,7 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32364
33028
|
});
|
|
32365
33029
|
};
|
|
32366
33030
|
const trackedQueryTransaction = (schema, use) => sql.withTransaction(Effect.gen(function* () {
|
|
33031
|
+
yield* prepareIndexes(schema).pipe(Effect.orDie);
|
|
32367
33032
|
const dependencies = makeDependencyRecorder();
|
|
32368
33033
|
const value = yield* use(Object.freeze({ db: makeReader(schema, dependencies) }));
|
|
32369
33034
|
const { revision: observedRevision } = yield* currentRevision(void 0).pipe(Effect.orDie);
|
|
@@ -32374,6 +33039,7 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32374
33039
|
};
|
|
32375
33040
|
}));
|
|
32376
33041
|
const trackedMutationTransaction = (schema, use) => sql.withTransaction(Effect.gen(function* () {
|
|
33042
|
+
yield* prepareIndexes(schema).pipe(Effect.orDie);
|
|
32377
33043
|
const invalidations = makeDependencyRecorder();
|
|
32378
33044
|
const value = yield* use(Object.freeze({ db: makeWriter(schema, invalidations) }));
|
|
32379
33045
|
const { revision: committedRevision } = yield* advanceRevision(void 0).pipe(Effect.orDie);
|
|
@@ -32954,20 +33620,22 @@ const agentAppFiles = [
|
|
|
32954
33620
|
path: "AGENTS.md"
|
|
32955
33621
|
},
|
|
32956
33622
|
{
|
|
32957
|
-
content: "---\nname: ignotum\ndescription: Build and modify an Ignotum app. Use when working on its schema, server functions, generated API, client UI, or local development workflow.\n---\n\n# Working on an Ignotum app\n\nAn Ignotum app defines a database schema, server functions, and a JSX client. Ignotum supplies the\ndatabase runtime, generated bindings, realtime query updates, Vite dev server, JSX runtime, and\nTailwind setup.\n\nThe current release supports local development. Do not assume that authentication, files, actions,\nworkflows, scheduled jobs, or production deployment APIs exist.\n\n## Start with the app\n\nInspect `server/schema.ts`, the function modules in `server`, the files in `client`, `package.json`,\nand `tsconfig.json` before changing code.\n\nThe required `client/index.tsx` default-exports `app({ title, component })` from `ignotum/client`.\nKeep the title as a non-empty quoted string. An optional `client/icon.svg` is discovered as the app\nfavicon. Do not add another metadata file or import Ignotum's Tailwind stylesheet.\n\nTreat `_generated` as compiler output. Read it when you need to understand an inferred type, but do\nnot edit it. After code changes, regenerate the bindings and run the typechecker:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nFor normal Ignotum app work, do not add an HTML file, Vite configuration, Tailwind configuration,\nAPI routes, or direct database setup.\n\n## Read the relevant reference\n\nThe bundled references are the Ignotum user guides. Read only the guides needed for the task:\n\n- For app creation and installation, read [getting started](references/getting-started.md) or\n [manual setup](references/manual-setup.md).\n- For tables, fields, IDs, and generated document types, read\n [schema syntax](references/schema.md).\n- For validators and their TypeScript types, read [values](references/values.md).\n- For queries, mutations,
|
|
33623
|
+
content: "---\nname: ignotum\ndescription: Build and modify an Ignotum app. Use when working on its schema, server functions, generated API, client UI, or local development workflow.\n---\n\n# Working on an Ignotum app\n\nAn Ignotum app defines a database schema, server functions, and a JSX client. Ignotum supplies the\ndatabase runtime, generated bindings, realtime query updates, Vite dev server, JSX runtime, and\nTailwind setup.\n\nThe current release supports local development. Do not assume that authentication, files, actions,\nworkflows, scheduled jobs, or production deployment APIs exist.\n\n## Start with the app\n\nInspect `server/schema.ts`, the function modules in `server`, the files in `client`, `package.json`,\nand `tsconfig.json` before changing code.\n\nThe required `client/index.tsx` default-exports `app({ title, component })` from `ignotum/client`.\nKeep the title as a non-empty quoted string. An optional `client/icon.svg` is discovered as the app\nfavicon. Do not add another metadata file or import Ignotum's Tailwind stylesheet.\n\nTreat `_generated` as compiler output. Read it when you need to understand an inferred type, but do\nnot edit it. After code changes, regenerate the bindings and run the typechecker:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nFor normal Ignotum app work, do not add an HTML file, Vite configuration, Tailwind configuration,\nAPI routes, or direct database setup.\n\n## Read the relevant reference\n\nThe bundled references are the Ignotum user guides. Read only the guides needed for the task:\n\n- For app creation and installation, read [getting started](references/getting-started.md) or\n [manual setup](references/manual-setup.md).\n- For tables, fields, indexes, IDs, and generated document types, read\n [schema syntax](references/schema.md).\n- For validators and their TypeScript types, read [values](references/values.md).\n- For queries, mutations, Results, and application errors, read\n [server functions](references/server-functions.md).\n- For document reads, indexes, ranges, ordering, pagination, and result methods, read\n [database reads](references/reading-data.md).\n- For inserts, patches, replacements, deletes, and transaction behavior, read\n [database writes](references/writing-data.md).\n- For hooks, query state, paginated lists, mutations, JSX, and Tailwind, read\n [client](references/client.md).\n- For development commands, code generation, flags, and local data, read\n [dev server](references/dev-server.md).\n- For the complete guide list, read the [documentation index](references/index.md).\n\n## Rules that cross guide boundaries\n\n- Define tables in `server/schema.ts`.\n- Put queries and mutations in TypeScript files directly inside `server`.\n- Import server builders from `@/_generated/server.js` and client references from\n `@/_generated/api.js`. Keep the `.js` suffix.\n- Put code used by both the client and server in `shared` and import it through `@/shared`.\n- Never edit `_generated`.\n- Use JSX and hooks from `ignotum/client`, not React or Preact packages directly.\n- Keep the app inside Ignotum's current model unless the user explicitly asks to move beyond it.\n",
|
|
32958
33624
|
path: ".agents/skills/ignotum/SKILL.md"
|
|
32959
33625
|
},
|
|
32960
33626
|
...[
|
|
32961
|
-
["client.md", "# Client\n\nIgnotum apps use JSX, hooks from `ignotum/client`, and Tailwind CSS. The required\n`client/index.tsx` file defines the browser title and root component:\n\n```tsx\nimport { app, Query, Result, useMutation, useQuery } from \"ignotum/client\";\n\nimport { api } from \"@/_generated/api.js\";\n\nfunction App() {\n return <main>My App</main>;\n}\n\nexport default app({\n title: \"My App\",\n component: App,\n});\n```\n\nThe title must be a non-empty quoted string in the `app(...)` definition so Ignotum can include it\nin the first HTML response.\n\n## Tailwind CSS\n\nIgnotum loads Tailwind CSS automatically. App code does not need to import a framework stylesheet.\n\nStyle JSX with Tailwind utility classes. Use the JSX `class` attribute:\n\n```tsx\nfunction App() {\n return (\n <main class=\"mx-auto max-w-xl px-6 py-16\">\n <h1 class=\"text-2xl font-semibold text-zinc-950\">Todos</h1>\n </main>\n );\n}\n```\n\nThe app does not need a Tailwind configuration file. Custom CSS files are ordinary client modules:\ngive them any filename and import them from app code when needed.\n\n## Public files\n\nPut files that need their own URL in a top-level `public` directory next to `client` and `server`.\nThe directory structure becomes the URL structure. For example, `public/documents/manual.pdf` is\navailable at `/documents/manual.pdf` after deployment.\n\nPublic files are limited to AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF. Ignotum checks that a file's\ncontents match its extension and rejects symbolic links, executable or code formats, and paths\nunder the reserved `_ignotum` name. Use `client/icon.svg` for the app favicon; arbitrary SVG files\nare not accepted in `public`. Put the directory at the app root—`client/public` is rejected.\n\n## Run a query\n\n`useQuery` takes a generated query reference. Pass the typed arguments when the query declares\nthem:\n\n```tsx\nconst todos = useQuery(api.todos.list);\nconst todo = useQuery(api.todos.get, { id });\nconst selectedTodo = useQuery(api.todos.get, id === undefined ? Query.skip : { id });\n```\n\n`Query.skip` keeps a query pending without opening a subscription. Use it when the arguments are\nnot available yet.\n\nThe first value is pending. Match every state with `Result.match`:\n\n```tsx\nreturn Result.match(todos, {\n pending: () => <p class=\"text-zinc-500\">Loading...</p>,\n value: (items) => (\n <ul>\n {items.map((todo) => (\n <li key={todo.id}>{todo.text}</li>\n ))}\n </ul>\n ),\n});\n```\n\nIgnotum keeps an active query up to date. A successful mutation refreshes affected subscribed\nqueries in every open client. Queries that did not read the changed documents or tables do not run\nagain.\n\n## Run a mutation\n\n`useMutation` takes a generated mutation reference and returns a function:\n\n```tsx\nconst createTodo = useMutation(api.todos.create);\n\nvoid createTodo({ text }).then(\n Result.match({\n value: (id) => console.log(id),\n error: {\n InvalidTodoText: ({ text }) => console.log(`Invalid text: ${text}`),\n TodoLimitReached: ({ limit }) => console.log(`The limit is ${limit}`),\n },\n internalError: ({ requestId }) => console.log(`Request ${requestId} failed.`),\n }),\n);\n```\n\nAn argument-free mutation returns a zero-argument function:\n\n```tsx\nconst clearTodos = useMutation(api.todos.clear);\nvoid clearTodos();\n```\n\nThe client `Result` API only inspects completed server responses. Server-only operations such as\n`Result.fail`, `Result.succeed`, `Result.try`, `yield*`, and `.catch()` are not available here.\n\nApplication errors are handled by `error`, either with one function or an exhaustive map keyed by\n`_tag`. An `InternalServerError` contains a request ID and is thrown when `Result.match` has no\n`internalError` handler. During rendering, it reaches the nearest UI error boundary. A mutation can\nhandle it locally with `internalError`, as above. Operation-scoped protocol failures reject mutation\npromises or reach the nearest query UI error boundary. Ignotum retries transient connection failures,\nkeeps the latest query state, and replays pending mutations after the next connection handshake.\n\nHosted apps limit connections, subscriptions, active unique queries, and mutation traffic. See\n[Limits](limits.md) for the current values and retry retention.\n"],
|
|
33627
|
+
["client.md", "# Client\n\nIgnotum apps use JSX, hooks from `ignotum/client`, and Tailwind CSS. The required\n`client/index.tsx` file defines the browser title and root component:\n\n```tsx\nimport { app, Query, Result, useMutation, usePaginatedQuery, useQuery } from \"ignotum/client\";\n\nimport { api } from \"@/_generated/api.js\";\n\nfunction App() {\n return <main>My App</main>;\n}\n\nexport default app({\n title: \"My App\",\n component: App,\n});\n```\n\nThe title must be a non-empty quoted string in the `app(...)` definition so Ignotum can include it\nin the first HTML response.\n\n## Tailwind CSS\n\nIgnotum loads Tailwind CSS automatically. App code does not need to import a framework stylesheet.\n\nStyle JSX with Tailwind utility classes. Use the JSX `class` attribute:\n\n```tsx\nfunction App() {\n return (\n <main class=\"mx-auto max-w-xl px-6 py-16\">\n <h1 class=\"text-2xl font-semibold text-zinc-950\">Todos</h1>\n </main>\n );\n}\n```\n\nThe app does not need a Tailwind configuration file. Custom CSS files are ordinary client modules:\ngive them any filename and import them from app code when needed.\n\n## Public files\n\nPut files that need their own URL in a top-level `public` directory next to `client` and `server`.\nThe directory structure becomes the URL structure. For example, `public/documents/manual.pdf` is\navailable at `/documents/manual.pdf` after deployment.\n\nPublic files are limited to AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF. Ignotum checks that a file's\ncontents match its extension and rejects symbolic links, executable or code formats, and paths\nunder the reserved `_ignotum` name. Use `client/icon.svg` for the app favicon; arbitrary SVG files\nare not accepted in `public`. Put the directory at the app root—`client/public` is rejected.\n\n## Run a query\n\n`useQuery` takes a generated query reference. Pass the typed arguments when the query declares\nthem:\n\n```tsx\nconst todos = useQuery(api.todos.list);\nconst todo = useQuery(api.todos.get, { id });\nconst selectedTodo = useQuery(api.todos.get, id === undefined ? Query.skip : { id });\n```\n\n`Query.skip` keeps a query pending without opening a subscription. Use it when the arguments are\nnot available yet.\n\nThe first value is pending. Match every state with `Result.match`:\n\n```tsx\nreturn Result.match(todos, {\n pending: () => <p class=\"text-zinc-500\">Loading...</p>,\n value: (items) => (\n <ul>\n {items.map((todo) => (\n <li key={todo.id}>{todo.text}</li>\n ))}\n </ul>\n ),\n});\n```\n\nIgnotum keeps an active query up to date. A successful mutation refreshes affected subscribed\nqueries in every open client. Queries that did not read the changed documents or tables do not run\nagain.\n\n## Load a paginated query\n\n`usePaginatedQuery` calls a query with a `pagination: values.pagination()` argument and a\n`values.page(...)` return value. Pass only the query's other arguments. The hook supplies the\ncursor and uses 20 items per page by default:\n\n```tsx\nconst events = usePaginatedQuery(api.events.list, {\n project: \"api\",\n level: \"error\",\n});\n\nreturn Result.match(events, {\n pending: () => <p>Loading...</p>,\n value: ({ items, loadMore, status }) => (\n <>\n {items.map((event) => (\n <p key={event.id}>{event.message}</p>\n ))}\n {status !== \"Exhausted\" && (\n <button disabled={status === \"LoadingMore\"} onClick={loadMore}>\n {status === \"LoadingMore\" ? \"Loading...\" : \"Load more\"}\n </button>\n )}\n </>\n ),\n});\n```\n\nPass `{ pageSize: number }` as the third argument to choose another size. The hook keeps earlier\nitems visible while it loads the next page. Changing the query arguments starts again from the\nfirst page. `Result.match` does not need another matcher because `LoadingMore` still has usable\nitems.\n\n## Run a mutation\n\n`useMutation` takes a generated mutation reference and returns a function:\n\n```tsx\nconst createTodo = useMutation(api.todos.create);\n\nvoid createTodo({ text }).then(\n Result.match({\n value: (id) => console.log(id),\n error: {\n InvalidTodoText: ({ text }) => console.log(`Invalid text: ${text}`),\n TodoLimitReached: ({ limit }) => console.log(`The limit is ${limit}`),\n },\n internalError: ({ requestId }) => console.log(`Request ${requestId} failed.`),\n }),\n);\n```\n\nAn argument-free mutation returns a zero-argument function:\n\n```tsx\nconst clearTodos = useMutation(api.todos.clear);\nvoid clearTodos();\n```\n\nThe client `Result` API only inspects completed server responses. Server-only operations such as\n`Result.fail`, `Result.succeed`, `Result.try`, `yield*`, and `.catch()` are not available here.\n\nApplication errors are handled by `error`, either with one function or an exhaustive map keyed by\n`_tag`. An `InternalServerError` contains a request ID and is thrown when `Result.match` has no\n`internalError` handler. During rendering, it reaches the nearest UI error boundary. A mutation can\nhandle it locally with `internalError`, as above. Operation-scoped protocol failures reject mutation\npromises or reach the nearest query UI error boundary. Ignotum retries transient connection failures,\nkeeps the latest query state, and replays pending mutations after the next connection handshake.\n\nHosted apps limit connections, subscriptions, active unique queries, and mutation traffic. See\n[Limits](limits.md) for the current values and retry retention.\n"],
|
|
32962
33628
|
["deploy.md", "# Deploy\n\nSet your API token in your shell:\n\n```sh\nexport IGNOTUM_API_TOKEN=your-token\n```\n\nDo not put the token in the app directory or commit it to source control.\n\nOn the first deploy, pass the app slug:\n\n```sh\nnpx ignotum deploy --app my-app\n```\n\nIf the slug belongs to one of your apps, Ignotum links it. Otherwise, Ignotum creates the app.\nWithout `--app`, the first deploy asks for the slug interactively.\n\nIgnotum stores the app ID, slug, and API URL in `.ignotum/app.json`. It never stores the access\ntoken there. Later deploys reuse the saved app, so the command has no app flag:\n\n```sh\nnpx ignotum deploy\n```\n\nThere is no separate build command. `deploy` generates the current bindings and builds both parts\nof the application, uploads the artifact, and makes the new deployment active.\n\nThe client output contains the SPA shell, generated assets, and validated files from the top-level\n`public` directory. The server output contains one bundle for every exported query or mutation.\nIgnotum writes both under `.ignotum/build` only after the complete build succeeds. If a rebuild\nfails, the previous successful output stays in place.\n\nUploads stream from disk. If an upload is interrupted after it begins, Ignotum leaves the active\ndeployment unchanged and prints the incomplete deployment ID. A rejected upload reports the\nAPI's reason. A successful deploy prints the app URL and deployment ID.\n\nThe CLI uses `https://api.ignotum.cloud` by default. Set `IGNOTUM_API_URL` only when targeting a\ndifferent Ignotum API, such as a local or development deployment.\n\nIgnotum rejects deployments that exceed the hosted file or artifact size limits. It also removes\nunfinished uploads and older inactive deployments after their retention periods. See\n[Limits](limits.md) for the current values.\n"],
|
|
32963
33629
|
["dev-server.md", "# Dev server\n\nRun the dev server from the app root:\n\n```sh\nnpx ignotum dev\n```\n\nIt expects these files:\n\n```text\nclient/index.tsx\nserver/schema.ts\n```\n\nThe dev server generates the client and server bindings, reads the `app(...)` definition from\n`client/index.tsx`, loads Tailwind automatically, and serves the app at\n<http://127.0.0.1:3210>. It reloads client and server changes and updates active queries after\nserver changes and successful mutations.\n\nYou do not need an HTML file, Vite configuration, Tailwind configuration, or framework stylesheet.\nAdd an optional `client/icon.svg` and Ignotum uses it as the favicon automatically. With no icon\nfile, the HTML contains no favicon link.\n\n## Flags\n\nUse flags to change the address or open the browser:\n\n```sh\nnpx ignotum dev --host 0.0.0.0 --port 3000 --open\n```\n\nThe defaults are host `127.0.0.1`, port `3210`, and no automatic browser opening.\n\n## Code generation\n\nThe dev server runs code generation when it starts. It updates generated files when you add or\nremove a server function file.\n\nRun code generation before typechecking without the dev server:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nIgnotum creates:\n\n- `_generated/server.ts` with schema-bound `query`, `mutation`, and `values` exports;\n- `_generated/api.ts` with client references such as `api.todos.list`;\n- `_generated/types.ts` with `DataModel`, `Doc`, and `Id`.\n\nDo not edit generated files.\n\n## Local data\n\nData persists between dev-server restarts. Stop the server and reset that data with:\n\n```sh\nnpx ignotum dev db reset\n```\n"],
|
|
32964
|
-
["getting-started.md", "# Getting started\n\nIgnotum requires Node.js 22.18 or newer.\n\nCreate an app:\n\n```sh\nnpx ignotum new my-app\ncd my-app\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. The generated app is a small counter with a schema, a query, a\nmutation, and a JSX client.\n\n`ignotum new` installs dependencies with pnpm when it is available. It falls back to npm only when\npnpm is not installed. It generates `_generated`, initializes a Git repository, and creates an\n`Init` commit containing the generated files after the rest of the setup finishes.\n\nPass `.` to create the app in the current directory. The directory must be empty:\n\n```sh\nnpx ignotum new .\n```\n\nUse `--no-git` to skip Git or `--no-install` to skip dependency installation. You can install the\ndependencies later with the same pnpm and npm fallback behavior:\n\n```sh\nnpx ignotum install\n```\n\nRead [schema syntax](schema.md), [server functions](server-functions.md), and the\n[client guide](client.md) to build the app. The [manual setup](manual-setup.md) recreates the counter\napp without `ignotum new`. When it is ready, follow the [deploy guide](deploy.md) to create or link\nthe hosted app and publish it.\n"],
|
|
32965
|
-
["index.md", "# Ignotum\n\nAn Ignotum app has a schema, server functions, and a client. Ignotum is opinionated about the\nclient tooling. Every app uses JSX, hooks from `ignotum/client`, and Tailwind CSS.\n\nThe current release supports local development and hosted deployment.\n\n- [Getting started](getting-started.md) creates and runs a counter app with `ignotum new`.\n- [Manual setup](manual-setup.md) recreates the generated counter app by hand.\n- [Schema syntax](schema.md) covers tables, fields, IDs, and generated document types.\n- [Values](values.md) lists every value validator and its TypeScript type.\n- [Server functions](server-functions.md) covers queries, mutations, database
|
|
32966
|
-
["limits.md", "# Limits\n\nThese limits apply to hosted Ignotum apps. The local dev server does not reproduce every hosted\nlimit, so an operation that works locally can still be rejected after deployment.\n\nIgnotum reports connection, traffic, time, result, and storage-quota failures as\n`ResourceLimitExceeded`. A database read or write that crosses a document or collection limit can\nappear as a temporary function failure. A deployment that exceeds a limit fails before activation,\nso the current deployment stays active.\n\n## Server functions\n\n| Limit | Value |\n| --------------------------------------- | ---------: |\n| Function arguments | 16 KiB |\n| Function result | 1 MiB |\n| Execution time | 10 seconds |\n| Memory | 32 MiB |\n| Stack | 512 KiB |\n| Ignotum operations during one execution | 1,000 |\n\nArgument and result sizes use their JSON representation. The result limit applies to successful\nresults and application errors.\n\nAn Ignotum operation is a call through the function context, such as a database read or write. A\nfunction stops when it reaches the execution time or operation limit.\n\nIgnotum retries one query execution when app data changes while it runs. That execution makes at\nmost four attempts and stops after 15 seconds. The query fails temporarily if it cannot read a\nconsistent result in that time.\n\n## App data\n\n| Limit | Value |\n| --------------------------------------- | -------------: |\n| Stored fields in one document | 256 KiB |\n| Documents returned by `collect()` | 1,000 |\n| Document fields returned by `collect()` | 1 MiB |\n| Stored app data | 64 MiB per app |\n\nThe 1 MiB function-result limit still applies to `collect()`. Document IDs and timestamps take some\nspace in that result, so a collection can reach the result limit before its fields reach 1 MiB.\n\nIf a mutation would take the app over its storage limit, Ignotum rolls back the whole mutation.\nThe quota covers stored documents and the records Ignotum keeps to process
|
|
32967
|
-
["manual-setup.md", "# Manual setup\n\nIgnotum requires Node.js 22.18 or newer. This guide recreates the counter app from `ignotum new`\nwithout running the app generator. It uses pnpm to install dependencies.\n\nCreate the app directory:\n\n```sh\nmkdir my-ignotum-app\ncd my-ignotum-app\n```\n\nCreate this structure:\n\n```text\nmy-ignotum-app/\n client/\n index.tsx\n icon.svg\n public/\n manual.pdf\n server/\n counter.ts\n schema.ts\n shared/\n utils.ts\n package.json\n tsconfig.json\n```\n\n## Configure the package\n\nAdd `package.json`:\n\n```json\n{\n \"name\": \"my-ignotum-app\",\n \"private\": true,\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"typecheck\": \"ignotum codegen && tsc --noEmit\"\n },\n \"dependencies\": {\n \"ignotum\": \"latest\"\n },\n \"devDependencies\": {\n \"typescript\": \"^7.0.2\"\n },\n \"engines\": {\n \"node\": \">=22.18.0\"\n }\n}\n```\n\nInstall the dependencies:\n\n```sh\npnpm install\n```\n\n## Configure TypeScript\n\nAdd `tsconfig.json`:\n\n```json\n{\n \"compilerOptions\": {\n \"target\": \"ES2023\",\n \"lib\": [\"ES2023\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"NodeNext\",\n \"moduleResolution\": \"NodeNext\",\n \"jsx\": \"react-jsx\",\n \"jsxImportSource\": \"ignotum/client\",\n \"strict\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true,\n \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"_generated\", \"client\", \"server\", \"shared\"]\n}\n```\n\n## Add shared code\n\nAdd `shared/utils.ts`:\n\n```ts\nexport const counterIncrement = 1;\n```\n\nBoth the client and server can import files in `shared` through `@/shared`.\n\n## Define the schema\n\nAdd `server/schema.ts`:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n counters: table({\n value: values.number(),\n }),\n}));\n```\n\n## Add the server functions\n\nAdd `server/counter.ts`:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\nimport { counterIncrement } from \"@/shared/utils.js\";\n\nexport const get = query({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n return counters[0]?.value ?? 0;\n },\n});\n\nexport const increment = mutation({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n const counter = counters[0];\n const value = (counter?.value ?? 0) + counterIncrement;\n\n if (counter === undefined) {\n yield* ctx.db.insert(\"counters\", { value });\n } else {\n yield* ctx.db.patch(\"counters\", counter.id, { value });\n }\n\n return value;\n },\n});\n```\n\nKeep the `.js` suffix on imports from `@/_generated`, even though the generated files use\nTypeScript.\n\n## Add the client\n\nAdd `client/index.tsx`:\n\n```tsx\nimport { app, Result, useMutation, useQuery } from \"ignotum/client\";\n\nimport { api } from \"@/_generated/api.js\";\nimport { counterIncrement } from \"@/shared/utils.js\";\n\nfunction App() {\n const count = useQuery(api.counter.get);\n const increment = useMutation(api.counter.increment);\n\n return (\n <main class=\"mx-auto max-w-sm px-6 py-20 text-center\">\n <h1 class=\"text-2xl font-semibold\">Counter</h1>\n {Result.match(count, {\n pending: () => <p class=\"mt-6\">Loading...</p>,\n value: (value) => (\n <>\n <p class=\"my-6 text-5xl tabular-nums\">{value}</p>\n <button\n class=\"rounded bg-zinc-900 px-4 py-2 text-white\"\n type=\"button\"\n onClick={() => void increment()}\n >\n Increment by {counterIncrement}\n </button>\n </>\n ),\n })}\n </main>\n );\n}\n\nexport default app({\n title: \"Counter\",\n component: App,\n});\n```\n\nIgnotum loads Tailwind automatically. You do not need an HTML file, Vite configuration, Tailwind\nconfiguration, or framework stylesheet. Custom CSS files are ordinary client modules and can use\nany filename when imported from app code.\n\nThe `client/icon.svg` file is optional. When present, Ignotum discovers it automatically and uses it\nas the favicon. When absent, the app has no favicon link.\n\nThe top-level `public` directory is optional. Its AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF files keep\ntheir relative paths as public URLs. Do not put this directory inside `client`.\n\n## Run the app\n\nStart the dev server:\n\n```sh\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. Ignotum creates `_generated` before starting the app.\n\nRead [schema syntax](schema.md), [server functions](server-functions.md), and the\n[client guide](client.md) to continue building the app.\n"],
|
|
32968
|
-
["
|
|
32969
|
-
["
|
|
32970
|
-
["
|
|
33630
|
+
["getting-started.md", "# Getting started\n\nIgnotum requires Node.js 22.18 or newer.\n\nCreate an app:\n\n```sh\nnpx ignotum new my-app\ncd my-app\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. The generated app is a small counter with a schema, a query, a\nmutation, and a JSX client.\n\n`ignotum new` installs dependencies with pnpm when it is available. It falls back to npm only when\npnpm is not installed. It generates `_generated`, initializes a Git repository, and creates an\n`Init` commit containing the generated files after the rest of the setup finishes.\n\nPass `.` to create the app in the current directory. The directory must be empty:\n\n```sh\nnpx ignotum new .\n```\n\nUse `--no-git` to skip Git or `--no-install` to skip dependency installation. You can install the\ndependencies later with the same pnpm and npm fallback behavior:\n\n```sh\nnpx ignotum install\n```\n\nRead [schema syntax](schema.md), [server functions](server-functions.md),\n[database reads](reading-data.md), [database writes](writing-data.md), and the\n[client guide](client.md) to build the app. The [manual setup](manual-setup.md) recreates the counter\napp without `ignotum new`. When it is ready, follow the [deploy guide](deploy.md) to create or link\nthe hosted app and publish it.\n"],
|
|
33631
|
+
["index.md", "# Ignotum\n\nAn Ignotum app has a schema, server functions, and a client. Ignotum is opinionated about the\nclient tooling. Every app uses JSX, hooks from `ignotum/client`, and Tailwind CSS.\n\nThe current release supports local development and hosted deployment.\n\n- [Getting started](getting-started.md) creates and runs a counter app with `ignotum new`.\n- [Manual setup](manual-setup.md) recreates the generated counter app by hand.\n- [Schema syntax](schema.md) covers tables, fields, IDs, and generated document types.\n- [Values](values.md) lists every value validator and its TypeScript type.\n- [Server functions](server-functions.md) covers queries, mutations, and application errors.\n- [Read from the database](reading-data.md) covers documents, indexes, ranges, ordering, and result methods.\n- [Write to the database](writing-data.md) covers inserts, patches, replacements, deletes, and mutation behavior.\n- [Client](client.md) covers queries, mutations, results, and Tailwind styling.\n- [Dev server](dev-server.md) covers local development, code generation, flags, and data reset.\n- [Deploy](deploy.md) creates or links an app, then uploads and activates a deployment.\n- [Limits](limits.md) lists hosted limits for functions, data, realtime updates, and deployments.\n"],
|
|
33632
|
+
["limits.md", "# Limits\n\nThese limits apply to hosted Ignotum apps. The local dev server does not reproduce every hosted\nlimit, so an operation that works locally can still be rejected after deployment.\n\nIgnotum reports connection, traffic, time, result, and storage-quota failures as\n`ResourceLimitExceeded`. A database read or write that crosses a document or collection limit can\nappear as a temporary function failure. A deployment that exceeds a limit fails before activation,\nso the current deployment stays active.\n\n## Server functions\n\n| Limit | Value |\n| --------------------------------------- | ---------: |\n| Function arguments | 16 KiB |\n| Function result | 1 MiB |\n| Execution time | 10 seconds |\n| Memory | 32 MiB |\n| Stack | 512 KiB |\n| Ignotum operations during one execution | 1,000 |\n\nArgument and result sizes use their JSON representation. The result limit applies to successful\nresults and application errors.\n\nAn Ignotum operation is a call through the function context, such as a database read or write. A\nfunction stops when it reaches the execution time or operation limit.\n\nIgnotum retries one query execution when app data changes while it runs. That execution makes at\nmost four attempts and stops after 15 seconds. The query fails temporarily if it cannot read a\nconsistent result in that time.\n\n## App data\n\n| Limit | Value |\n| --------------------------------------- | -------------: |\n| Stored fields in one document | 256 KiB |\n| Documents returned by `collect()` | 1,000 |\n| Documents returned by `take()` | 1,000 |\n| Documents returned by one page | 1,000 |\n| Document fields returned by `collect()` | 1 MiB |\n| Stored app data | 64 MiB per app |\n\nThe 1 MiB function-result limit still applies to `collect()`. Document IDs and timestamps take some\nspace in that result, so a collection can reach the result limit before its fields reach 1 MiB.\n\nIf a mutation would take the app over its storage limit, Ignotum rolls back the whole mutation.\nThe quota covers stored documents, application indexes, and the records Ignotum keeps to process\nmutations safely. Deployment files do not count as stored app data.\n\n## Schema indexes\n\n| Limit | Value |\n| ---------------------------- | -------------: |\n| Indexes on one table | 16 |\n| Indexes in one schema | 128 |\n| Declared fields in one index | 8 |\n| Index name | 64 UTF-8 bytes |\n| One encoded index key | 4 KiB |\n\nEvery index also includes `createdAt` and `id` after its declared fields. The key limit applies to\nthe combined encoded field values and those final ordering values. A document must fit every index\ndeclared for its table.\n\n## Realtime connections and calls\n\n| Limit | Value |\n| ------------------------- | ---------------------------: |\n| Live connections | 256 per app |\n| Incoming realtime message | 64 KiB |\n| Subscriptions | 128 per connection |\n| Active unique queries | 64 per app |\n| Realtime query refresh | 4 attempts within 15 seconds |\n| Concurrent operations | 32 per app |\n| Mutation execution | 1 at a time per app |\n| Unresolved mutations | 32 per app |\n| Mutation calls | 60 per minute per app |\n\nA unique query is one function and argument combination. Several components or browser tabs can\nsubscribe to the same unique query without using another unique-query slot.\n\nA realtime refresh can repeat a query execution when app data changes again during the refresh.\n\nThe unresolved-mutation limit protects calls whose outcome is not known yet, such as a call waiting\nfor a retry after a connection failure. Once Ignotum receives a final result, that call no longer\ncounts toward the limit.\n\nIgnotum runs mutations for the same app one at a time. Queries and other live calls can still use\nthe remaining concurrent-operation slots.\n\n## Mutation retries\n\nThe client keeps the ID of a pending mutation and reuses it after a reconnect. Ignotum remembers up\nto 10,000 mutation results per app for seven days. During that period, a retry returns the recorded\nresult instead of running the mutation again.\n\nDo not treat this as permanent duplicate protection. A mutation may run again after its record has\nexpired or fallen outside the 10,000 most recent results.\n\n## Deployments\n\n| Limit | Value |\n| ------------------------------ | ------: |\n| Files listed in one deployment | 512 |\n| One listed file | 16 MiB |\n| Deployment inventory | 1 MiB |\n| Server files combined | 64 MiB |\n| Listed files combined | 128 MiB |\n\nIgnotum never deletes the active deployment. It also protects the three newest deployments that\ncompleted successfully. An older inactive deployment becomes eligible for deletion after seven\ndays. An unfinished upload becomes eligible after 24 hours.\n"],
|
|
33633
|
+
["manual-setup.md", "# Manual setup\n\nIgnotum requires Node.js 22.18 or newer. This guide recreates the counter app from `ignotum new`\nwithout running the app generator. It uses pnpm to install dependencies.\n\nCreate the app directory:\n\n```sh\nmkdir my-ignotum-app\ncd my-ignotum-app\n```\n\nCreate this structure:\n\n```text\nmy-ignotum-app/\n client/\n index.tsx\n icon.svg\n public/\n manual.pdf\n server/\n counter.ts\n schema.ts\n shared/\n utils.ts\n package.json\n tsconfig.json\n```\n\n## Configure the package\n\nAdd `package.json`:\n\n```json\n{\n \"name\": \"my-ignotum-app\",\n \"private\": true,\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"typecheck\": \"ignotum codegen && tsc --noEmit\"\n },\n \"dependencies\": {\n \"ignotum\": \"latest\"\n },\n \"devDependencies\": {\n \"typescript\": \"^7.0.2\"\n },\n \"engines\": {\n \"node\": \">=22.18.0\"\n }\n}\n```\n\nInstall the dependencies:\n\n```sh\npnpm install\n```\n\n## Configure TypeScript\n\nAdd `tsconfig.json`:\n\n```json\n{\n \"compilerOptions\": {\n \"target\": \"ES2023\",\n \"lib\": [\"ES2023\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"NodeNext\",\n \"moduleResolution\": \"NodeNext\",\n \"jsx\": \"react-jsx\",\n \"jsxImportSource\": \"ignotum/client\",\n \"strict\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true,\n \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"_generated\", \"client\", \"server\", \"shared\"]\n}\n```\n\n## Add shared code\n\nAdd `shared/utils.ts`:\n\n```ts\nexport const counterIncrement = 1;\n```\n\nBoth the client and server can import files in `shared` through `@/shared`.\n\n## Define the schema\n\nAdd `server/schema.ts`:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n counters: table({\n value: values.number(),\n }),\n}));\n```\n\n## Add the server functions\n\nAdd `server/counter.ts`:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\nimport { counterIncrement } from \"@/shared/utils.js\";\n\nexport const get = query({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n return counters[0]?.value ?? 0;\n },\n});\n\nexport const increment = mutation({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n const counter = counters[0];\n const value = (counter?.value ?? 0) + counterIncrement;\n\n if (counter === undefined) {\n yield* ctx.db.insert(\"counters\", { value });\n } else {\n yield* ctx.db.patch(\"counters\", counter.id, { value });\n }\n\n return value;\n },\n});\n```\n\nKeep the `.js` suffix on imports from `@/_generated`, even though the generated files use\nTypeScript.\n\n## Add the client\n\nAdd `client/index.tsx`:\n\n```tsx\nimport { app, Result, useMutation, useQuery } from \"ignotum/client\";\n\nimport { api } from \"@/_generated/api.js\";\nimport { counterIncrement } from \"@/shared/utils.js\";\n\nfunction App() {\n const count = useQuery(api.counter.get);\n const increment = useMutation(api.counter.increment);\n\n return (\n <main class=\"mx-auto max-w-sm px-6 py-20 text-center\">\n <h1 class=\"text-2xl font-semibold\">Counter</h1>\n {Result.match(count, {\n pending: () => <p class=\"mt-6\">Loading...</p>,\n value: (value) => (\n <>\n <p class=\"my-6 text-5xl tabular-nums\">{value}</p>\n <button\n class=\"rounded bg-zinc-900 px-4 py-2 text-white\"\n type=\"button\"\n onClick={() => void increment()}\n >\n Increment by {counterIncrement}\n </button>\n </>\n ),\n })}\n </main>\n );\n}\n\nexport default app({\n title: \"Counter\",\n component: App,\n});\n```\n\nIgnotum loads Tailwind automatically. You do not need an HTML file, Vite configuration, Tailwind\nconfiguration, or framework stylesheet. Custom CSS files are ordinary client modules and can use\nany filename when imported from app code.\n\nThe `client/icon.svg` file is optional. When present, Ignotum discovers it automatically and uses it\nas the favicon. When absent, the app has no favicon link.\n\nThe top-level `public` directory is optional. Its AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF files keep\ntheir relative paths as public URLs. Do not put this directory inside `client`.\n\n## Run the app\n\nStart the dev server:\n\n```sh\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. Ignotum creates `_generated` before starting the app.\n\nRead [schema syntax](schema.md), [server functions](server-functions.md),\n[database reads](reading-data.md), [database writes](writing-data.md), and the\n[client guide](client.md) to continue building the app.\n"],
|
|
33634
|
+
["reading-data.md", "# Read from the database\n\nQuery and mutation handlers can read documents through `ctx.db`. Every read is explicit: a table\nquery reads that table, while an indexed query follows an index declared in the schema. Ignotum\ndoes not silently load a table and filter it to make an unindexed operation look efficient.\n\n## Read one document\n\nUse `find` when a missing document is an ordinary result:\n\n```ts\nconst todo = yield * ctx.db.find(\"todos\", args.id);\n// Todo | undefined\n```\n\nUse `get` when the document should exist:\n\n```ts\nconst todo = yield * ctx.db.get(\"todos\", args.id);\n// Todo\n```\n\nA missing `get` fails with `DocumentNotFound`, which contains the table and ID. You can expose it,\ncatch it, or map it to one of your own application errors.\n\n## Read a table\n\n`collect` returns every document in the query:\n\n```ts\nconst todos = yield * ctx.db.query(\"todos\").collect();\n```\n\nThis is the right API when the table is intentionally small or the function genuinely needs every\ndocument. It is a full table read, not an indexed lookup. Hosted collection limits still apply.\n\nQueries use ascending `createdAt` order by default, with `id` breaking ties. Reverse the order with\n`order(\"desc\")`:\n\n```ts\nconst newestFirst = yield * ctx.db.query(\"todos\").order(\"desc\").take(20);\n```\n\n## Read through an index\n\nDeclare indexes in the schema, then select one by name:\n\n```ts\n// server/schema.ts\ntodos: table({\n channel: values.string(),\n priority: values.integer(),\n text: values.string(),\n}).index(\"by_channel_priority\", [\"channel\", \"priority\"]);\n```\n\n```ts\nconst todos =\n yield *\n ctx.db\n .query(\"todos\")\n .index(\"by_channel_priority\", (range) =>\n range.eq(\"channel\", args.channel).gte(\"priority\", args.minimumPriority),\n )\n .collect();\n```\n\nRange fields must follow the index order. Use equality on any leading fields, then optionally add a\nlower bound, an upper bound, or both on the next field:\n\n| Method | Matches |\n| ------------------- | -------------------------------- |\n| `eq(field, value)` | Equal to `value` |\n| `gt(field, value)` | Greater than `value` |\n| `gte(field, value)` | Greater than or equal to `value` |\n| `lt(field, value)` | Less than `value` |\n| `lte(field, value)` | Less than or equal to `value` |\n\nFor a two-sided range, chain the bounds on the same field:\n\n```ts\nconst thisWeek =\n yield *\n ctx.db\n .query(\"events\")\n .index(\"by_workspace_start\", (range) =>\n range\n .eq(\"workspaceId\", args.workspaceId)\n .gte(\"start\", args.weekStart)\n .lt(\"start\", args.nextWeek),\n )\n .collect();\n```\n\nCalling `index` without a range scans that index in its declared order:\n\n```ts\nconst byPriority = yield * ctx.db.query(\"todos\").index(\"by_priority\").collect();\n```\n\nEvery index orders by its declared fields, then `createdAt`, then `id`. The two system fields are\navailable as range fields after all declared fields have been matched with `eq`.\n\n## Choose how many documents to return\n\nAll table and index queries support these terminal methods:\n\n| Method | Result |\n| ------------------- | ---------------------------------------------------------------------- |\n| `collect()` | Every matching document |\n| `take(number)` | At most `number` matching documents |\n| `first()` | The first matching document, or `undefined` |\n| `unique()` | The only matching document, `undefined`, or a failure if several match |\n| `paginate(options)` | One page and a cursor for the next page |\n\n`unique()` checks a query result; it does not add a uniqueness constraint to the index. Use\n`first()` when several matches are valid and only the first one matters.\n\nUse `take` or `first` whenever the function only needs a bounded result. Use `collect` when reading\nthe complete matching set is intentional.\n\n## Read pages\n\nAdd a `pagination` argument and return a page validator when a client should load a long ordered\nlist in parts:\n\n```ts\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n args: {\n completed: values.boolean(),\n pagination: values.pagination(),\n },\n returns: values.page(Todo),\n\n handler: function* (ctx, { completed, pagination }) {\n return yield* ctx.db\n .query(\"todos\")\n .index(\"by_completed\", (range) => range.eq(\"completed\", completed))\n .order(\"desc\")\n .paginate(pagination);\n },\n});\n```\n\nA page contains `items` and `nextCursor`. A `null` cursor means there are no more matching\ndocuments. Pass a non-null cursor back through `pagination` to continue the same table, index,\nrange, and order. Cursors are opaque and must not be parsed or changed.\n\nPage sizes are integers from 1 through 1,000. Prefer an index for paginated filters so the database\ncan read the requested range directly.\n\n## Reads in mutations\n\nMutation handlers receive the same read API and observe their earlier writes in that mutation.\nThis makes read-modify-write logic straightforward:\n\n```ts\nconst todo = yield * ctx.db.get(\"todos\", args.id);\nyield * ctx.db.patch(\"todos\", todo.id, { completed: !todo.completed });\n```\n\nSee [Write to the database](writing-data.md) for the write methods and transaction behavior.\n"],
|
|
33635
|
+
["schema.md", "# Schema syntax\n\nDefine the data model in `server/schema.ts`. The keys returned from `defineSchema` are table\nnames, and each `table` call defines that table's fields:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n users: table({\n name: values.string(),\n }),\n todos: table({\n text: values.string(),\n completed: values.boolean(),\n ownerId: values.optional(values.id(\"users\")),\n }).index(\"by_completed\", [\"completed\"]),\n}));\n```\n\n## Field values\n\nRead [values](values.md) for the complete validator list and the TypeScript type produced by each\none. `values.id` only accepts a table declared in the same schema. Arrays and objects can be nested,\nand their contents can use any value validator.\n\n## System fields\n\nIgnotum adds three fields to every stored document:\n\n| Field | Type |\n| ----------- | ------------------------- |\n| `id` | The ID type for its table |\n| `createdAt` | `Date` |\n| `updatedAt` | `Date` |\n\nDo not declare these fields in a table. Do not pass them to `insert`, `patch`, or `replace`.\n\n## Indexes\n\nChain `index` after a table to declare an ordered index. The first argument is its name; the second\nis the field order:\n\n```ts\nmessages: table({\n channel: values.string(),\n authorId: values.id(\"users\"),\n text: values.string(),\n})\n .index(\"by_channel\", [\"channel\"])\n .index(\"by_channel_author\", [\"channel\", \"authorId\"]);\n```\n\nIndex fields must be required booleans, dates, IDs, integers, numbers, strings, or literals. Field\norder matters: `by_channel_author` can efficiently select one channel, or one channel and author,\nbut it is not an author-only index.\n\nEvery index uses `createdAt` and `id` as its final ordering fields. Read\n[Read from the database](reading-data.md) for selecting indexes, building ranges, ordering results,\nand choosing a result method.\n\n## Generated types\n\n`_generated/types.ts` exports the data model, document, and ID types:\n\n```ts\nimport type { DataModel, Doc, Id } from \"@/_generated/types.js\";\n\ntype Todo = Doc<\"todos\">;\ntype TodoId = Id<\"todos\">;\n```\n\n`Doc<\"todos\">` includes the fields from the `todos` table and its three system fields. An\n`Id<\"todos\">` cannot be passed where an `Id<\"users\">` is required.\n"],
|
|
33636
|
+
["server-functions.md", "# Server functions\n\nPut queries and mutations in `.ts` files directly inside `server`. The file name becomes the API\nmodule, and each exported function keeps its export name:\n\n```text\nserver/todos.ts -> api.todos.list\nserver/users.ts -> api.users.get\n```\n\nIgnotum ignores `schema.ts`, `index.ts`, test files, and names beginning with `_`.\n\nImport schema-bound builders from the generated server file. Import `Result` only when the module\nintroduces or catches typed errors:\n\n```ts\nimport { Result } from \"ignotum/server\";\n\nimport { mutation, query, values } from \"@/_generated/server.js\";\n```\n\nKeep the `.js` suffix on generated imports.\n\n## Define a function\n\nA function has optional argument, return, and public error schemas, plus a generator handler:\n\n```ts\nexport const getTitle = query({\n args: {\n id: values.id(\"todos\"),\n },\n returns: values.string(),\n\n handler: function* (ctx, args) {\n const todo = yield* ctx.db.get(\"todos\", args.id);\n return todo.text;\n },\n});\n```\n\nOmit `args` when the function takes no arguments. Omit `returns` when it returns nothing. An\nomitted `returns` only permits a `void` handler; Ignotum never infers an unchecked return schema.\n`yield*` waits for an Ignotum operation and propagates its typed application errors. Return\nsuccessful values with ordinary `return`.\n\nIgnotum validates arguments before running the handler. It also validates returned values and\npublic application errors before sending them to a client.\n\nAn argument-free query and a mutation with no return value can stay small:\n\n```ts\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n returns: values.array(Todo),\n\n handler: function* (ctx) {\n return yield* ctx.db.query(\"todos\").collect();\n },\n});\n\nexport const remove = mutation({\n args: { id: values.id(\"todos\") },\n\n handler: function* (ctx, args) {\n yield* ctx.db.delete(\"todos\", args.id);\n },\n});\n```\n\nCall an argument-free query as `useQuery(api.todos.list)`. An argument-free mutation returns a\nzero-argument function:\n\n```ts\nconst clear = useMutation(api.todos.clear);\nvoid clear();\n```\n\n## Database access\n\nQuery handlers receive the read API. Mutation handlers receive the same reads plus writes, and all\nof a mutation's writes commit together.\n\n- [Read from the database](reading-data.md) covers `find`, `get`, table and index queries, ranges,\n ordering, `collect`, `take`, `first`, and `unique`.\n- [Write to the database](writing-data.md) covers `insert`, `patch`, `replace`, `delete`, and\n mutation behavior.\n\nHosted apps limit document size, collection reads, function runtime, and stored app data. See\n[Limits](limits.md) for the current values.\n\n## Application errors\n\nDefine an application error with `values.error`. Its name becomes `_tag`:\n\n```ts\nconst TodoNotFound = values.error(\"TodoNotFound\", {\n id: values.id(\"todos\"),\n});\n```\n\n`Result.fail` deliberately stops the operation with a typed error:\n\n```ts\nyield * Result.fail(TodoNotFound({ id: args.id }));\n```\n\nThe `errors` field is optional. If omitted, Ignotum infers the handler's remaining application\nerrors. If supplied, it is the public contract and the handler must conform to it:\n\n```ts\nexport const toggle = mutation({\n args: {\n id: values.id(\"todos\"),\n },\n returns: values.boolean(),\n errors: TodoNotFound,\n\n handler: function* (ctx, args) {\n const todo = yield* ctx.db.get(\"todos\", args.id).catch({\n DocumentNotFound: (error) => Result.fail(TodoNotFound({ id: error.id })),\n });\n\n const completed = !todo.completed;\n yield* ctx.db.patch(\"todos\", args.id, { completed });\n return completed;\n },\n});\n```\n\nCombine public errors with `values.union`:\n\n```ts\nerrors: values.union(InvalidTodoText, TodoLimitReached),\n```\n\n## Catch and recover\n\nEvery Result operation has a partial, tag-based `catch`. Handlers receive the narrowed error type.\nUnmatched errors continue through the channel:\n\n```ts\nconst settings =\n yield *\n loadSettings().catch({\n SettingsNotFound: () => defaultSettings,\n });\n```\n\nReturn a plain value to recover. Return `Result.fail(...)` to map one error to another. Unknown tag\nnames fail the TypeScript check.\n\nUse `Result.try` for one catch boundary around several operations:\n\n```ts\nconst author =\n yield *\n Result.try(function* () {\n const membership = yield* ctx.db.get(\"memberships\", topic.membershipId);\n return yield* ctx.db.get(\"users\", membership.userId);\n }).catch({\n DocumentNotFound: () => Result.fail(InvalidAuthor({ topicId: topic.id })),\n });\n```\n\nThere is no async variant. Ignotum operations always use `yield*` in server code.\n\n## Standalone results\n\n`Result.succeed` remains useful for helpers that return a Result:\n\n```ts\nfunction validateName(name: string) {\n if (name.length === 0) {\n return Result.fail(InvalidName({}));\n }\n\n return Result.succeed(name.trim());\n}\n```\n\nA handler can use the helper with `const name = yield* validateName(args.name)`. Normal handlers do\nnot wrap successful returns in `Result.succeed`.\n\n## Internal failures and defects\n\nDatabase outages, internal runtime failures, and thrown JavaScript exceptions are not application\nerrors. Ignotum logs their full cause and sends only:\n\n```ts\n{\n _tag: \"InternalServerError\",\n requestId: \"...\",\n}\n```\n\nEvery generated client function includes `InternalServerError` in its Result error union. The\nrequest ID links the client-visible failure to server logs without exposing private details.\n`InternalServerError` is reserved by Ignotum: never define it with `values.error` or include it in a\nfunction's `errors` schema.\n"],
|
|
33637
|
+
["values.md", "# Values\n\nUse `values` validators to describe table fields, function arguments, return values, and application\nerrors. Each validator checks values at runtime and supplies the matching TypeScript type.\n\n| Validator | TypeScript type | Notes |\n| ----------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- |\n| `values.string()` | `string` | |\n| `values.number()` | `number` | Accepts finite JavaScript numbers, including integers. |\n| `values.integer()` | `number` | Accepts safe integers. |\n| `values.boolean()` | `boolean` | |\n| `values.date()` | `Date` | Accepts valid JavaScript dates. |\n| `values.null()` | `null` | Accepts only `null`. |\n| `values.literal(value)` | The exact type of `value` | Accepts one string, finite number, or boolean value. |\n| `values.literals(first, second, ...rest)` | A union of the supplied literal types | Requires at least two string, finite number, or boolean values. |\n| `values.id(\"todos\")` | `Id<\"todos\">` | Accepts an ID for a table declared in the same schema. IDs for different tables are different types. |\n| `values.doc(\"todos\")` | `Doc<\"todos\">` | Accepts a complete document, including its `id`, `createdAt`, and `updatedAt` system fields. |\n| `values.pagination()` | Pagination options | Defines the `pagination` argument used by `.paginate(...)`. |\n| `values.page(value)` | A page of `T` | Defines the return value of a paginated query. |\n| `values.optional(value)` | `T \\| undefined` | Makes an object or table field optional. The field may be omitted. |\n| `values.nullable(value)` | `T \\| null` | The value remains required unless it is also wrapped with `optional`. |\n| `values.array(value)` | `ReadonlyArray<T>` | Every item must match `value`. |\n| `values.object(fields)` | An object matching `fields` | Defines an object with known field names. |\n| `values.record(value)` | `Readonly<Record<string, T>>` | Defines an object with dynamic string keys whose values all match `value`. |\n| `values.union(...values)` | A union of the supplied types | Accepts a value matching any supplied validator. |\n| `values.never()` | `never` | No value can pass this validator. |\n| `values.error(\"Name\", fields)` | A tagged error object | Defines an application error whose `_tag` is the supplied name. |\n\n`T` means the TypeScript type produced by the wrapped validator.\n\n`values.doc`, `values.pagination`, and `values.page` are available on the schema-bound `values`\nexported by `_generated/server.ts`. They are not available while defining `server/schema.ts`.\n\n## Object transforms\n\nValidators created by `values.object(fields)` and `values.doc(\"table\")` support these chainable\nmethods:\n\n| Method | Result |\n| ------------------- | -------------------------------------------------------------------- |\n| `.pick(...keys)` | Keeps the listed fields. Every key must exist. |\n| `.omit(...keys)` | Removes the listed fields. Every key must exist. |\n| `.extend(fields)` | Adds fields. It rejects names that already exist. |\n| `.override(fields)` | Replaces validators for existing fields. It rejects new field names. |\n| `.partial()` | Makes every current field optional. |\n\nEach method works from the result of the previous method. Added fields can be picked, omitted, or\noverridden immediately. An omitted name can be added again with a different validator.\n\n```ts\nconst TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial().extend({\n requestId: values.string(),\n});\n```\n\nUse `override` when changing an existing field. This makes replacements visible in the definition\nand prevents `extend` from silently weakening fields such as `id`:\n\n```ts\nconst EditableTodo = values.doc(\"todos\").override({\n title: values.optional(values.string()),\n});\n```\n\nThese methods are only available on fixed object validators. Arrays, records, unions, errors, and\nprimitive validators do not expose them. After transforming a document validator, its inferred type\nmatches the current fields in the chain rather than the complete document type.\n\n### Reuse embedded objects in the schema\n\nCreate a fixed object validator inside the `defineSchema` callback when several stored fields share\nan object shape. A transform can derive a stored variant without repeating its fields:\n\n```ts\ndefineSchema(({ table, values }) => {\n const Contact = values.object({\n email: values.string(),\n phone: values.string(),\n });\n\n return {\n users: table({\n contact: Contact,\n }),\n publicProfiles: table({\n contact: Contact.omit(\"phone\"),\n }),\n };\n});\n```\n\nThis pattern suits embedded objects stored by more than one table, including full and reduced\nversions of the same object. The base validator stays inside `defineSchema`, where `values.id` can\ncheck its table references against the completed schema.\n\n### Derive server function validators from documents\n\nUse the schema-bound `values` from `_generated/server.ts` when a function input or output follows a\nstored document. These validators know every table name and the complete document fields, including\n`id`, `createdAt`, and `updatedAt`.\n\nReusable server validators can live in an ignored server module such as `server/_validators.ts`:\n\n```ts\nimport { values } from \"@/_generated/server.js\";\n\nexport const TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial();\n\nexport const PublicTodo = values.doc(\"todos\").omit(\"updatedAt\");\n```\n\nImport these validators into queries and mutations that need the same contract. This works well for\npatch inputs and document projections. Do not import generated validators into `server/schema.ts`.\nDocument validators depend on the schema, so importing them while defining that schema would create\na cycle.\n\n## Examples\n\n```ts\nconst TodoStatus = values.literals(\"pending\", \"completed\");\n\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n returns: values.array(Todo),\n handler: function* (ctx) {\n return yield* ctx.db.query(\"todos\").collect();\n },\n});\n```\n\nUse the other validators to define reusable values that do not represent a stored document:\n\n```ts\nconst TodoInput = values.object({\n status: TodoStatus,\n scheduledAt: values.nullable(values.date()),\n scores: values.record(values.integer()),\n title: values.string(),\n});\n```\n\nUse `optional` when a field may be absent. Use `nullable` when a present field may contain `null`:\n\n```ts\nvalues.object({\n nickname: values.optional(values.string()),\n deletedAt: values.nullable(values.date()),\n});\n```\n"],
|
|
33638
|
+
["writing-data.md", "# Write to the database\n\nMutation handlers can insert, patch, replace, and delete documents through `ctx.db`. Query handlers\nonly receive the read API.\n\n## Insert\n\n`insert` creates a document and returns its table-specific ID:\n\n```ts\nconst id =\n yield *\n ctx.db.insert(\"todos\", {\n text: \"Learn Ignotum\",\n completed: false,\n });\n```\n\nSupply every required table field. Omit optional fields when they have no value. Ignotum supplies\n`id`, `createdAt`, and `updatedAt`; never pass those system fields yourself.\n\n## Patch\n\n`patch` changes only the supplied fields:\n\n```ts\nyield * ctx.db.patch(\"todos\", args.id, { completed: true });\n```\n\nAll other fields keep their current values. Patching a missing document has no effect.\n\n## Replace\n\n`replace` supplies a new complete set of application fields:\n\n```ts\nyield *\n ctx.db.replace(\"todos\", args.id, {\n text: \"Build an app\",\n completed: false,\n });\n```\n\nEvery required field must be present. The document keeps its `id` and `createdAt`, while\n`updatedAt` advances. Replacing a missing document has no effect.\n\n## Delete\n\n`delete` removes a document by ID:\n\n```ts\nyield * ctx.db.delete(\"todos\", args.id);\n```\n\nDeleting a missing document has no effect.\n\n## Mutation behavior\n\nA mutation sees its earlier writes, including through indexed reads. Ignotum commits all of the\nmutation's writes together after the handler succeeds. If the handler fails with an application\nerror or encounters an internal failure, none of its writes are committed.\n\n```ts\nexport const completeOldest = mutation({\n handler: function* (ctx) {\n const todo = yield* ctx.db\n .query(\"todos\")\n .index(\"by_completed\", (range) => range.eq(\"completed\", false))\n .first();\n\n if (todo !== undefined) {\n yield* ctx.db.patch(\"todos\", todo.id, { completed: true });\n }\n },\n});\n```\n\nRead [Read from the database](reading-data.md) for `find`, `get`, table queries, indexed ranges,\nordering, and result methods.\n"]
|
|
32971
33639
|
].map(([name, content]) => ({
|
|
32972
33640
|
content,
|
|
32973
33641
|
path: `.agents/skills/ignotum/references/${name}`
|