ignotum 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -101
- package/dist/cli/bin.mjs +1487 -168
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/runtime/{api-DtX8qPrq.js → api-BTeMI5tx.js} +27 -3
- package/dist/runtime/api-BTeMI5tx.js.map +1 -0
- package/dist/runtime/{api-D2bsAz4T.d.ts → api-D9lV-5av.d.ts} +7 -10
- package/dist/runtime/client.d.ts +27 -3
- package/dist/runtime/client.js +468 -68
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/{descriptor-t6BOEGw9-BTHR-ZMv.js → descriptor-C5VA9qRl-C338iC6l.js} +63 -5
- package/dist/runtime/descriptor-C5VA9qRl-C338iC6l.js.map +1 -0
- package/dist/runtime/file-BXf63ulU.js +166 -0
- package/dist/runtime/file-BXf63ulU.js.map +1 -0
- package/dist/runtime/{result-BgcHn25t.d.ts → id-Btwac71X-DGj0DQuu.d.ts} +56 -4
- package/dist/runtime/{index-Dl_VQGmA.d.ts → index-CF04_Dps.d.ts} +96 -86
- package/dist/runtime/internal/api.d.ts +2 -2
- package/dist/runtime/internal/api.js +1 -1
- package/dist/runtime/internal/host.d.ts +25 -8
- package/dist/runtime/internal/host.js +209 -15
- 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-BNFhAjns.d.ts +1 -0
- package/dist/runtime/pagination-DrOowBve-Bnipd34u.d.ts +84 -0
- package/dist/runtime/{schema-B-jMZEbs.js → schema-DJfwfq87.js} +108 -30
- package/dist/runtime/schema-DJfwfq87.js.map +1 -0
- package/dist/runtime/server.d.ts +3 -3
- package/dist/runtime/server.js +2 -2
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/sync-mIXn9eKv.d.ts +8 -0
- package/package.json +2 -2
- package/src/cli/agent-files.ts +56 -13
- package/src/cli/app-configuration.ts +4 -1
- package/src/cli/build/server.ts +83 -6
- package/src/cli/new-app.ts +15 -0
- package/src/client/files.ts +168 -0
- package/src/client/hooks.ts +166 -17
- package/src/client/index.ts +9 -1
- package/src/client/sync.ts +137 -21
- package/src/dev-runtime/database.ts +621 -78
- package/src/dev-runtime/files.ts +338 -0
- package/src/dev-runtime/functions.ts +14 -6
- package/src/dev-runtime/migrations.ts +44 -0
- package/src/dev-runtime/sync.ts +123 -5
- package/src/internal/api.ts +16 -1
- package/src/server/index.ts +8 -1
- package/dist/runtime/api-DtX8qPrq.js.map +0 -1
- package/dist/runtime/descriptor-t6BOEGw9-BTHR-ZMv.js.map +0 -1
- package/dist/runtime/id-Btwac71X-B9OeBzvq.d.ts +0 -9
- package/dist/runtime/id-D570vudg.js +0 -26
- package/dist/runtime/id-D570vudg.js.map +0 -1
- package/dist/runtime/schema-B-jMZEbs.js.map +0 -1
package/dist/cli/bin.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import * as Path$3 from "effect/Path";
|
|
|
11
11
|
import { Path, TypeId } from "effect/Path";
|
|
12
12
|
import * as Predicate$1 from "effect/Predicate";
|
|
13
13
|
import * as Sink from "effect/Sink";
|
|
14
|
-
import * as Stream from "effect/Stream";
|
|
14
|
+
import * as Stream$1 from "effect/Stream";
|
|
15
15
|
import * as ChildProcess$1 from "effect/unstable/process/ChildProcess";
|
|
16
16
|
import { ChildProcessSpawner, ExitCode, ProcessId, make, makeHandle } from "effect/unstable/process/ChildProcessSpawner";
|
|
17
17
|
import * as NodeChildProcess from "node:child_process";
|
|
@@ -69,7 +69,7 @@ import * as RcRef from "effect/RcRef";
|
|
|
69
69
|
import * as Terminal$1 from "effect/Terminal";
|
|
70
70
|
import * as readline from "node:readline";
|
|
71
71
|
import * as Runtime from "effect/Runtime";
|
|
72
|
-
import { Array as Array$1, Brand, Cause, Config, Context, Crypto, DateTime, Effect, Effectable, Encoding, FiberSet, FileSystem, Function as Function$1, HashMap, HashSet, Layer, ManagedRuntime, MutableHashMap, MutableHashSet, Option, Path as Path$1, Predicate, PubSub, Ref, Result, Schedule, Schema, Semaphore, String as String$1, Terminal } from "effect";
|
|
72
|
+
import { Array as Array$1, Brand, Cause, Config, Context, Crypto, DateTime, Effect, Effectable, Encoding, FiberSet, FileSystem, Function as Function$1, HashMap, HashSet, Layer, ManagedRuntime, MutableHashMap, MutableHashSet, Option, Path as Path$1, Predicate, PubSub, Ref, Result, Schedule, Schema, SchemaGetter, Semaphore, Stream, String as String$1, Terminal } from "effect";
|
|
73
73
|
import { Argument, CliError, Command, Flag, Prompt } from "effect/unstable/cli";
|
|
74
74
|
import { customAlphabet } from "nanoid";
|
|
75
75
|
import { HttpClient, HttpClientRequest, HttpClientResponse, HttpServerRequest as HttpServerRequest$1, HttpServerResponse } from "effect/unstable/http";
|
|
@@ -251,7 +251,7 @@ const pullIntoWritable = (options) => options.pull.pipe(Effect$1.flatMap((chunk)
|
|
|
251
251
|
* @category constructors
|
|
252
252
|
* @since 4.0.0
|
|
253
253
|
*/
|
|
254
|
-
const fromReadable = (options) => Stream.fromChannel(fromReadableChannel(options));
|
|
254
|
+
const fromReadable = (options) => Stream$1.fromChannel(fromReadableChannel(options));
|
|
255
255
|
/**
|
|
256
256
|
* Creates a `Channel` that pulls chunks from a Node readable stream, mapping
|
|
257
257
|
* errors with `onError` and destroying the readable on completion unless
|
|
@@ -406,7 +406,7 @@ var StreamAdapter = class extends Readable {
|
|
|
406
406
|
constructor(context, stream) {
|
|
407
407
|
super({});
|
|
408
408
|
this.readLatch = Latch.makeUnsafe(false);
|
|
409
|
-
this.fiber = Stream.runForEachArray(stream, (chunk) => this.readLatch.whenOpen(Effect$1.sync(() => {
|
|
409
|
+
this.fiber = Stream$1.runForEachArray(stream, (chunk) => this.readLatch.whenOpen(Effect$1.sync(() => {
|
|
410
410
|
this.readLatch.closeUnsafe();
|
|
411
411
|
for (let i = 0; i < chunk.length; i++) {
|
|
412
412
|
const item = chunk[i];
|
|
@@ -462,7 +462,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
462
462
|
...options.env
|
|
463
463
|
} : options.env;
|
|
464
464
|
};
|
|
465
|
-
const inputToStdioOption = (input) => Stream.isStream(input) ? "pipe" : input;
|
|
465
|
+
const inputToStdioOption = (input) => Stream$1.isStream(input) ? "pipe" : input;
|
|
466
466
|
const outputToStdioOption = (input) => Sink.isSink(input) ? "pipe" : input;
|
|
467
467
|
const resolveStdinOption = (options) => {
|
|
468
468
|
const defaultConfig = {
|
|
@@ -475,7 +475,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
475
475
|
...defaultConfig,
|
|
476
476
|
stream: options.stdin
|
|
477
477
|
};
|
|
478
|
-
if (Stream.isStream(options.stdin)) return {
|
|
478
|
+
if (Stream$1.isStream(options.stdin)) return {
|
|
479
479
|
...defaultConfig,
|
|
480
480
|
stream: options.stdin
|
|
481
481
|
};
|
|
@@ -519,7 +519,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
519
519
|
const setupAdditionalFds = Effect$1.fnUntraced(function* (command, childProcess, additionalFds) {
|
|
520
520
|
if (additionalFds.length === 0) return {
|
|
521
521
|
getInputFd: () => Sink.drain,
|
|
522
|
-
getOutputFd: () => Stream.empty
|
|
522
|
+
getOutputFd: () => Stream$1.empty
|
|
523
523
|
};
|
|
524
524
|
const inputSinks = /* @__PURE__ */ new Map();
|
|
525
525
|
const outputStreams = /* @__PURE__ */ new Map();
|
|
@@ -532,12 +532,12 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
532
532
|
evaluate: () => nodeStream,
|
|
533
533
|
onError: (error) => toPlatformError(`fromWritable(fd${fd})`, toError(error), command)
|
|
534
534
|
});
|
|
535
|
-
if (config.stream) yield* Effect$1.forkScoped(Stream.run(config.stream, sink));
|
|
535
|
+
if (config.stream) yield* Effect$1.forkScoped(Stream$1.run(config.stream, sink));
|
|
536
536
|
inputSinks.set(fd, sink);
|
|
537
537
|
break;
|
|
538
538
|
}
|
|
539
539
|
case "output": {
|
|
540
|
-
let stream = Stream.empty;
|
|
540
|
+
let stream = Stream$1.empty;
|
|
541
541
|
if (nodeStream && "read" in nodeStream) {
|
|
542
542
|
const passThrough = new PassThrough();
|
|
543
543
|
nodeStream.on("error", (error) => passThrough.destroy(error));
|
|
@@ -547,7 +547,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
547
547
|
onError: (error) => toPlatformError(`fromReadable(fd${fd})`, toError(error), command)
|
|
548
548
|
});
|
|
549
549
|
}
|
|
550
|
-
if (config.sink) stream = Stream.transduce(stream, config.sink);
|
|
550
|
+
if (config.sink) stream = Stream$1.transduce(stream, config.sink);
|
|
551
551
|
outputStreams.set(fd, stream);
|
|
552
552
|
break;
|
|
553
553
|
}
|
|
@@ -555,7 +555,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
555
555
|
}
|
|
556
556
|
return {
|
|
557
557
|
getInputFd: (fd) => inputSinks.get(fd) ?? Sink.drain,
|
|
558
|
-
getOutputFd: (fd) => outputStreams.get(fd) ?? Stream.empty
|
|
558
|
+
getOutputFd: (fd) => outputStreams.get(fd) ?? Stream$1.empty
|
|
559
559
|
};
|
|
560
560
|
});
|
|
561
561
|
const setupChildStdin = (command, childProcess, config) => Effect$1.suspend(() => {
|
|
@@ -566,7 +566,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
566
566
|
endOnDone: config.endOnDone,
|
|
567
567
|
encoding: config.encoding
|
|
568
568
|
});
|
|
569
|
-
if (Stream.isStream(config.stream)) return Effect$1.as(Effect$1.forkScoped(Stream.run(config.stream, sink)), sink);
|
|
569
|
+
if (Stream$1.isStream(config.stream)) return Effect$1.as(Effect$1.forkScoped(Stream$1.run(config.stream, sink)), sink);
|
|
570
570
|
return Effect$1.succeed(sink);
|
|
571
571
|
});
|
|
572
572
|
const setupChildOutputStreams = (command, childProcess, stdoutConfig, stderrConfig) => {
|
|
@@ -578,7 +578,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
578
578
|
evaluate: () => passThrough,
|
|
579
579
|
onError: (error) => toPlatformError("fromReadable(stdout)", toError(error), command)
|
|
580
580
|
});
|
|
581
|
-
})() : Stream.empty;
|
|
581
|
+
})() : Stream$1.empty;
|
|
582
582
|
let stderr = childProcess.stderr ? (() => {
|
|
583
583
|
const passThrough = new PassThrough();
|
|
584
584
|
childProcess.stderr.on("error", (error) => passThrough.destroy(error));
|
|
@@ -587,10 +587,10 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
587
587
|
evaluate: () => passThrough,
|
|
588
588
|
onError: (error) => toPlatformError("fromReadable(stderr)", toError(error), command)
|
|
589
589
|
});
|
|
590
|
-
})() : Stream.empty;
|
|
591
|
-
if (Sink.isSink(stdoutConfig.stream)) stdout = Stream.transduce(stdout, stdoutConfig.stream);
|
|
592
|
-
if (Sink.isSink(stderrConfig.stream)) stderr = Stream.transduce(stderr, stderrConfig.stream);
|
|
593
|
-
const all = Stream.merge(stdout, stderr);
|
|
590
|
+
})() : Stream$1.empty;
|
|
591
|
+
if (Sink.isSink(stdoutConfig.stream)) stdout = Stream$1.transduce(stdout, stdoutConfig.stream);
|
|
592
|
+
if (Sink.isSink(stderrConfig.stream)) stderr = Stream$1.transduce(stderr, stderrConfig.stream);
|
|
593
|
+
const all = Stream$1.merge(stdout, stderr);
|
|
594
594
|
return {
|
|
595
595
|
stdout,
|
|
596
596
|
stderr,
|
|
@@ -744,7 +744,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
744
744
|
const command = pipeline[i];
|
|
745
745
|
const options = pipeOptions[i] ?? {};
|
|
746
746
|
const stdinConfig = resolveStdinOption(command.options);
|
|
747
|
-
const sourceStream = Stream.unwrap(Effect$1.succeed(getSourceStream(handles[handles.length - 1], options.from)));
|
|
747
|
+
const sourceStream = Stream$1.unwrap(Effect$1.succeed(getSourceStream(handles[handles.length - 1], options.from)));
|
|
748
748
|
const toOption = options.to ?? "stdin";
|
|
749
749
|
if (toOption === "stdin") handles.push(yield* spawnCommand(ChildProcess$1.make(command.command, command.args, {
|
|
750
750
|
...command.options,
|
|
@@ -4759,7 +4759,7 @@ const utimes = /*#__PURE__*/ (() => {
|
|
|
4759
4759
|
const nodeUtimes = /*#__PURE__*/ effectify(NFS.utimes, /*#__PURE__*/ handleErrnoException("FileSystem", "utime"), /*#__PURE__*/ handleBadArgument("utime"));
|
|
4760
4760
|
return (path, atime, mtime) => nodeUtimes(path, atime, mtime);
|
|
4761
4761
|
})();
|
|
4762
|
-
const watchNode = (path, options) => Stream.callback((queue) => Effect$1.acquireRelease(Effect$1.sync(() => {
|
|
4762
|
+
const watchNode = (path, options) => Stream$1.callback((queue) => Effect$1.acquireRelease(Effect$1.sync(() => {
|
|
4763
4763
|
const watcher = NFS.watch(path, { recursive: options?.recursive ?? false }, (event, path) => {
|
|
4764
4764
|
if (!path) return;
|
|
4765
4765
|
switch (event) {
|
|
@@ -4797,7 +4797,7 @@ const watchNode = (path, options) => Stream.callback((queue) => Effect$1.acquire
|
|
|
4797
4797
|
});
|
|
4798
4798
|
return watcher;
|
|
4799
4799
|
}), (watcher) => Effect$1.sync(() => watcher.close())));
|
|
4800
|
-
const watch = (backend, path, options) => stat(path).pipe(Effect$1.map((stat) => backend.pipe(Option$1.flatMap((_) => _.register(path, stat, options)), Option$1.getOrElse(() => watchNode(path, options)))), Stream.unwrap);
|
|
4800
|
+
const watch = (backend, path, options) => stat(path).pipe(Effect$1.map((stat) => backend.pipe(Option$1.flatMap((_) => _.register(path, stat, options)), Option$1.getOrElse(() => watchNode(path, options)))), Stream$1.unwrap);
|
|
4801
4801
|
const writeFile$1 = (path, data, options) => Effect$1.callback((resume, signal) => {
|
|
4802
4802
|
try {
|
|
4803
4803
|
NFS.writeFile(path, data, {
|
|
@@ -28742,7 +28742,7 @@ const stream = (source, headers) => Multipart.makeConfig(headers).pipe(Effect$1.
|
|
|
28742
28742
|
return parser;
|
|
28743
28743
|
},
|
|
28744
28744
|
onError: (error) => convertError(error)
|
|
28745
|
-
})), Stream.unwrap, Stream.map(convertPart));
|
|
28745
|
+
})), Stream$1.unwrap, Stream$1.map(convertPart));
|
|
28746
28746
|
/**
|
|
28747
28747
|
* Parses multipart data from a Node readable request body and persists file
|
|
28748
28748
|
* parts using the current `FileSystem`, `Path`, and `Scope` services.
|
|
@@ -29305,7 +29305,7 @@ const handleResponse = (request, response) => {
|
|
|
29305
29305
|
nodeResponse.writeHead(response.status, headers);
|
|
29306
29306
|
const drainLatch = Latch.makeUnsafe();
|
|
29307
29307
|
nodeResponse.on("drain", () => drainLatch.openUnsafe());
|
|
29308
|
-
return body.stream.pipe(Stream.orDie, Stream.runForEachArray((array) => {
|
|
29308
|
+
return body.stream.pipe(Stream$1.orDie, Stream$1.runForEachArray((array) => {
|
|
29309
29309
|
const chunk = array.length > 1 ? Buffer.concat(array) : array[0];
|
|
29310
29310
|
if (nodeResponse.write(chunk)) return Effect$1.void;
|
|
29311
29311
|
drainLatch.closeUnsafe();
|
|
@@ -29407,6 +29407,124 @@ const DevDatabaseLockId = defineId("lock", "ignotum/dev/DatabaseLockId");
|
|
|
29407
29407
|
const InvocationKey = Schema.String.pipe(Schema.brand("ignotum/hosted/InvocationKey"));
|
|
29408
29408
|
const DeploymentGeneration = Schema.Natural.pipe(Schema.brand("ignotum/hosted/DeploymentGeneration"));
|
|
29409
29409
|
const AppStateRevision = Schema.Natural.pipe(Schema.brand("ignotum/hosted/AppStateRevision"));
|
|
29410
|
+
const FileFormat = Schema.Literals([
|
|
29411
|
+
"jpeg",
|
|
29412
|
+
"png",
|
|
29413
|
+
"webp",
|
|
29414
|
+
"avif",
|
|
29415
|
+
"gif"
|
|
29416
|
+
]);
|
|
29417
|
+
const fileMimeTypes = {
|
|
29418
|
+
avif: "image/avif",
|
|
29419
|
+
gif: "image/gif",
|
|
29420
|
+
jpeg: "image/jpeg",
|
|
29421
|
+
png: "image/png",
|
|
29422
|
+
webp: "image/webp"
|
|
29423
|
+
};
|
|
29424
|
+
const fileLimits = {
|
|
29425
|
+
activeGrantsPerConnection: 1024,
|
|
29426
|
+
distinctFilesPerQuerySnapshot: 256,
|
|
29427
|
+
fileBytes: 10485760,
|
|
29428
|
+
filesPerMutation: 8,
|
|
29429
|
+
filenameBytes: 255,
|
|
29430
|
+
mutationBytes: 26214400,
|
|
29431
|
+
preparationLifetimeMillis: 9e5,
|
|
29432
|
+
stagedBytesPerApp: 104857600
|
|
29433
|
+
};
|
|
29434
|
+
const FileId = Object.assign(Schema.String.pipe(Schema.check(Schema.isPattern(/^file_[0-9A-Za-z_-]{20,128}$/)), Schema.brand("ignotum/file/FileId")), { idPrefix: "file" });
|
|
29435
|
+
const FileUploadToken = defineId("upload", "ignotum/file/FileUploadToken");
|
|
29436
|
+
const FileGrantToken = defineId("grant", "ignotum/file/FileGrantToken");
|
|
29437
|
+
const FileValueTypeId = Symbol.for("ignotum/file/FileValue");
|
|
29438
|
+
const FileGrantTypeId = Symbol.for("ignotum/file/FileGrant");
|
|
29439
|
+
const isFileValue = (value) => Predicate.isObject(value) && Predicate.hasProperty(value, FileValueTypeId) && Predicate.isObject(value[FileValueTypeId]) && Predicate.hasProperty(value[FileValueTypeId], "id") && Schema.is(FileId)(value[FileValueTypeId].id) && Predicate.hasProperty(value, "format") && Schema.is(FileFormat)(value.format) && Predicate.hasProperty(value, "name") && Predicate.isString(value.name) && Predicate.hasProperty(value, "size") && Predicate.isNumber(value.size) && Predicate.hasProperty(value, "mimeType") && value.mimeType === fileMimeTypes[value.format];
|
|
29440
|
+
const buildFileValue = (id, metadata, grantUrl) => {
|
|
29441
|
+
const value = {
|
|
29442
|
+
format: metadata.format,
|
|
29443
|
+
mimeType: fileMimeTypes[metadata.format],
|
|
29444
|
+
name: metadata.name,
|
|
29445
|
+
size: metadata.size
|
|
29446
|
+
};
|
|
29447
|
+
Object.defineProperty(value, FileValueTypeId, {
|
|
29448
|
+
configurable: false,
|
|
29449
|
+
enumerable: false,
|
|
29450
|
+
value: Object.freeze({ id }),
|
|
29451
|
+
writable: false
|
|
29452
|
+
});
|
|
29453
|
+
if (grantUrl !== void 0) Object.defineProperty(value, FileGrantTypeId, {
|
|
29454
|
+
configurable: false,
|
|
29455
|
+
enumerable: false,
|
|
29456
|
+
value: grantUrl,
|
|
29457
|
+
writable: false
|
|
29458
|
+
});
|
|
29459
|
+
return Object.freeze(value);
|
|
29460
|
+
};
|
|
29461
|
+
const makeFileValue = (id, metadata) => buildFileValue(id, metadata);
|
|
29462
|
+
const fileIdOf = (value) => value[FileValueTypeId].id;
|
|
29463
|
+
const EncodedFileValue = Schema.Struct({
|
|
29464
|
+
$ignotum: Schema.Literal("file"),
|
|
29465
|
+
id: FileId,
|
|
29466
|
+
format: FileFormat,
|
|
29467
|
+
name: Schema.String,
|
|
29468
|
+
size: Schema.Int.check(Schema.isBetween({
|
|
29469
|
+
minimum: 0,
|
|
29470
|
+
maximum: fileLimits.fileBytes
|
|
29471
|
+
}))
|
|
29472
|
+
});
|
|
29473
|
+
const RuntimeFileMetadata = Schema.Struct({
|
|
29474
|
+
id: FileId,
|
|
29475
|
+
format: FileFormat,
|
|
29476
|
+
name: Schema.String,
|
|
29477
|
+
size: Schema.Int.check(Schema.isBetween({
|
|
29478
|
+
minimum: 0,
|
|
29479
|
+
maximum: fileLimits.fileBytes
|
|
29480
|
+
}))
|
|
29481
|
+
});
|
|
29482
|
+
const FilePath = Schema.Array(Schema.Union([Schema.String, Schema.Natural]));
|
|
29483
|
+
const RuntimeFileOccurrence = Schema.Struct({
|
|
29484
|
+
path: FilePath,
|
|
29485
|
+
file: RuntimeFileMetadata
|
|
29486
|
+
});
|
|
29487
|
+
const DecodedFileValue = Schema.declare(isFileValue, { title: "FileValue" });
|
|
29488
|
+
const FileValue = EncodedFileValue.pipe(Schema.decodeTo(DecodedFileValue, {
|
|
29489
|
+
decode: SchemaGetter.transform((encoded) => makeFileValue(encoded.id, {
|
|
29490
|
+
format: encoded.format,
|
|
29491
|
+
name: encoded.name,
|
|
29492
|
+
size: encoded.size
|
|
29493
|
+
})),
|
|
29494
|
+
encode: SchemaGetter.transform((value) => ({
|
|
29495
|
+
$ignotum: "file",
|
|
29496
|
+
format: value.format,
|
|
29497
|
+
id: fileIdOf(value),
|
|
29498
|
+
name: value.name,
|
|
29499
|
+
size: value.size
|
|
29500
|
+
}))
|
|
29501
|
+
}));
|
|
29502
|
+
const collectEncodedFileOccurrences = (value, path, occurrences) => {
|
|
29503
|
+
if (Schema.is(EncodedFileValue)(value)) {
|
|
29504
|
+
const file = Schema.decodeUnknownSync(EncodedFileValue)(value);
|
|
29505
|
+
occurrences.push({
|
|
29506
|
+
path,
|
|
29507
|
+
file: {
|
|
29508
|
+
format: file.format,
|
|
29509
|
+
id: file.id,
|
|
29510
|
+
name: file.name,
|
|
29511
|
+
size: file.size
|
|
29512
|
+
}
|
|
29513
|
+
});
|
|
29514
|
+
return;
|
|
29515
|
+
}
|
|
29516
|
+
if (globalThis.Array.isArray(value)) {
|
|
29517
|
+
for (const [index, child] of value.entries()) collectEncodedFileOccurrences(child, [...path, index], occurrences);
|
|
29518
|
+
return;
|
|
29519
|
+
}
|
|
29520
|
+
if (!Schema.is(Schema.JsonObject)(value)) return;
|
|
29521
|
+
for (const [key, child] of Object.entries(value)) collectEncodedFileOccurrences(child, [...path, key], occurrences);
|
|
29522
|
+
};
|
|
29523
|
+
const encodedFileOccurrencesOf = (value) => {
|
|
29524
|
+
const occurrences = [];
|
|
29525
|
+
collectEncodedFileOccurrences(value, [], occurrences);
|
|
29526
|
+
return occurrences;
|
|
29527
|
+
};
|
|
29410
29528
|
//#endregion
|
|
29411
29529
|
//#region ../contracts/dist/runtime/value.js
|
|
29412
29530
|
const TransportValueSchema = Schema.suspend(() => Schema.Union([
|
|
@@ -29415,6 +29533,7 @@ const TransportValueSchema = Schema.suspend(() => Schema.Union([
|
|
|
29415
29533
|
Schema.Finite,
|
|
29416
29534
|
Schema.String,
|
|
29417
29535
|
Schema.Date,
|
|
29536
|
+
FileValue,
|
|
29418
29537
|
Schema.Array(TransportValueSchema),
|
|
29419
29538
|
Schema.Record(Schema.String, Schema.UndefinedOr(TransportValueSchema))
|
|
29420
29539
|
]));
|
|
@@ -29424,6 +29543,7 @@ const collectDatePaths = (value, path, paths) => {
|
|
|
29424
29543
|
paths.push(path);
|
|
29425
29544
|
return;
|
|
29426
29545
|
}
|
|
29546
|
+
if (isFileValue(value)) return;
|
|
29427
29547
|
if (globalThis.Array.isArray(value)) {
|
|
29428
29548
|
for (const [index, child] of value.entries()) collectDatePaths(child, [...path, index], paths);
|
|
29429
29549
|
return;
|
|
@@ -29476,9 +29596,18 @@ const Invoke = Schema.Struct({
|
|
|
29476
29596
|
function: FunctionAddress,
|
|
29477
29597
|
args: Schema.Json
|
|
29478
29598
|
});
|
|
29599
|
+
const PrepareMutation = Schema.Struct({
|
|
29600
|
+
type: Schema.Literal("PrepareMutation"),
|
|
29601
|
+
id: InvocationId,
|
|
29602
|
+
kind: Schema.Literal("Mutation"),
|
|
29603
|
+
function: FunctionAddress,
|
|
29604
|
+
args: Schema.Json,
|
|
29605
|
+
files: Schema.Array(RuntimeFileOccurrence)
|
|
29606
|
+
});
|
|
29479
29607
|
const ClientMessage = Schema.Union([
|
|
29480
29608
|
Subscribe,
|
|
29481
29609
|
Unsubscribe,
|
|
29610
|
+
PrepareMutation,
|
|
29482
29611
|
Invoke
|
|
29483
29612
|
]);
|
|
29484
29613
|
const SubscriptionOperation = Schema.Struct({
|
|
@@ -29513,7 +29642,21 @@ const Snapshot = Schema.Struct({
|
|
|
29513
29642
|
type: Schema.Literal("Snapshot"),
|
|
29514
29643
|
id: SubscriptionId,
|
|
29515
29644
|
result: WireResult,
|
|
29516
|
-
revision: AppStateRevision
|
|
29645
|
+
revision: AppStateRevision,
|
|
29646
|
+
files: Schema.optional(Schema.Array(Schema.Struct({
|
|
29647
|
+
path: RuntimeFileOccurrence.fields.path,
|
|
29648
|
+
file: RuntimeFileMetadata,
|
|
29649
|
+
url: Schema.String
|
|
29650
|
+
})))
|
|
29651
|
+
});
|
|
29652
|
+
const MutationPrepared = Schema.Struct({
|
|
29653
|
+
type: Schema.Literal("MutationPrepared"),
|
|
29654
|
+
id: InvocationId,
|
|
29655
|
+
uploads: Schema.Array(Schema.Struct({
|
|
29656
|
+
path: RuntimeFileOccurrence.fields.path,
|
|
29657
|
+
file: RuntimeFileMetadata,
|
|
29658
|
+
url: Schema.optional(Schema.String)
|
|
29659
|
+
}))
|
|
29517
29660
|
});
|
|
29518
29661
|
const SyncResultSuccess = Schema.Struct({
|
|
29519
29662
|
type: Schema.Literal("Result"),
|
|
@@ -29550,6 +29693,7 @@ const DeploymentChanged = Schema.Struct({
|
|
|
29550
29693
|
const ServerMessage = Schema.Union([
|
|
29551
29694
|
SyncHandshake,
|
|
29552
29695
|
Snapshot,
|
|
29696
|
+
MutationPrepared,
|
|
29553
29697
|
SyncResultSuccess,
|
|
29554
29698
|
SyncResultFailure,
|
|
29555
29699
|
ProtocolError,
|
|
@@ -29580,7 +29724,7 @@ var FunctionUnavailable = class extends Schema.TaggedError()("FunctionUnavailabl
|
|
|
29580
29724
|
message: Schema.String
|
|
29581
29725
|
}) {};
|
|
29582
29726
|
//#endregion
|
|
29583
|
-
//#region ../contracts/dist/descriptor-
|
|
29727
|
+
//#region ../contracts/dist/descriptor-C5VA9qRl.js
|
|
29584
29728
|
const LiteralValue = Schema.Union([
|
|
29585
29729
|
Schema.String,
|
|
29586
29730
|
Schema.Finite,
|
|
@@ -29597,6 +29741,17 @@ const ValueDescriptor = Schema.Union([
|
|
|
29597
29741
|
}),
|
|
29598
29742
|
Schema.Struct({ type: Schema.Literal("boolean") }),
|
|
29599
29743
|
Schema.Struct({ type: Schema.Literal("date") }),
|
|
29744
|
+
Schema.Struct({
|
|
29745
|
+
type: Schema.Literal("file"),
|
|
29746
|
+
formats: Schema.Array(Schema.Literals([
|
|
29747
|
+
"jpeg",
|
|
29748
|
+
"png",
|
|
29749
|
+
"webp",
|
|
29750
|
+
"avif",
|
|
29751
|
+
"gif"
|
|
29752
|
+
])),
|
|
29753
|
+
maxBytes: Schema.Int
|
|
29754
|
+
}),
|
|
29600
29755
|
Schema.Struct({
|
|
29601
29756
|
type: Schema.Literal("error"),
|
|
29602
29757
|
tag: Schema.String,
|
|
@@ -29650,6 +29805,53 @@ Function$1.dual(2, (value, descriptor) => {
|
|
|
29650
29805
|
});
|
|
29651
29806
|
return value;
|
|
29652
29807
|
});
|
|
29808
|
+
const getValueDescriptor = (value) => {
|
|
29809
|
+
if (!Predicate.hasProperty(value, ValueDescriptorTypeId)) return void 0;
|
|
29810
|
+
return Schema.is(ValueDescriptor)(value[ValueDescriptorTypeId]) ? value[ValueDescriptorTypeId] : void 0;
|
|
29811
|
+
};
|
|
29812
|
+
const descriptorContainsFile = (descriptor) => {
|
|
29813
|
+
switch (descriptor.type) {
|
|
29814
|
+
case "file": return true;
|
|
29815
|
+
case "array":
|
|
29816
|
+
case "nullable":
|
|
29817
|
+
case "optional":
|
|
29818
|
+
case "record": return descriptorContainsFile(descriptor.value);
|
|
29819
|
+
case "error":
|
|
29820
|
+
case "object": return descriptor.fields.some((field) => descriptorContainsFile(field.value));
|
|
29821
|
+
case "union": return descriptor.members.some(descriptorContainsFile);
|
|
29822
|
+
default: return false;
|
|
29823
|
+
}
|
|
29824
|
+
};
|
|
29825
|
+
const descriptorFields = (fields) => {
|
|
29826
|
+
const entries = [];
|
|
29827
|
+
for (const name of Reflect.ownKeys(fields)) {
|
|
29828
|
+
if (!Predicate.isString(name)) throw new Error("Ignotum value objects only support string field names.");
|
|
29829
|
+
const field = fields[name];
|
|
29830
|
+
const value = field === void 0 ? void 0 : getValueDescriptor(field);
|
|
29831
|
+
if (value === void 0) throw new Error(`The field '${name}' must use an Ignotum values validator.`);
|
|
29832
|
+
entries.push({
|
|
29833
|
+
name,
|
|
29834
|
+
value
|
|
29835
|
+
});
|
|
29836
|
+
}
|
|
29837
|
+
return entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
29838
|
+
};
|
|
29839
|
+
//#endregion
|
|
29840
|
+
//#region ../contracts/dist/versioned.js
|
|
29841
|
+
/** Registers the first revision of a long-lived format. */
|
|
29842
|
+
const initial = (schema) => schema;
|
|
29843
|
+
/** Adds one adjacent migration and keeps encoding on the new current revision. */
|
|
29844
|
+
const upgrade = (previous, current, migrate) => {
|
|
29845
|
+
const accepted = Schema.Union([previous, current]);
|
|
29846
|
+
const isCurrent = Schema.is(current);
|
|
29847
|
+
return accepted.pipe(Schema.decodeTo(Schema.toType(current), {
|
|
29848
|
+
decode: SchemaGetter.transform((value) => {
|
|
29849
|
+
if (isCurrent(value)) return value;
|
|
29850
|
+
return migrate(value);
|
|
29851
|
+
}),
|
|
29852
|
+
encode: SchemaGetter.transform((value) => value)
|
|
29853
|
+
}));
|
|
29854
|
+
};
|
|
29653
29855
|
//#endregion
|
|
29654
29856
|
//#region ../contracts/dist/deployment.js
|
|
29655
29857
|
const ArtifactPath = Schema.String.check(Schema.isPattern(/^(?!\/)(?![A-Za-z]:\/)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*(?:^|\/)\.(?:\/|$))(?!.*\/\/)[^\\\0]+$/)).pipe(Schema.brand("ignotum/deployment/ArtifactPath"));
|
|
@@ -29683,15 +29885,15 @@ const ClientRoute = Schema.Struct({
|
|
|
29683
29885
|
pathname: ClientPath,
|
|
29684
29886
|
artifact: ArtifactReference
|
|
29685
29887
|
});
|
|
29686
|
-
const DeploymentInventory = Schema.Struct({
|
|
29888
|
+
const DeploymentInventory = initial(Schema.Struct({
|
|
29687
29889
|
formatVersion: Schema.Literal(1),
|
|
29688
29890
|
files: Schema.Array(ArtifactFile)
|
|
29689
|
-
});
|
|
29690
|
-
const ClientManifest = Schema.Struct({
|
|
29891
|
+
}));
|
|
29892
|
+
const ClientManifest = initial(Schema.Struct({
|
|
29691
29893
|
formatVersion: Schema.Literal(1),
|
|
29692
29894
|
shell: ArtifactReference,
|
|
29693
29895
|
routes: Schema.Array(ClientRoute)
|
|
29694
|
-
});
|
|
29896
|
+
}));
|
|
29695
29897
|
const ClientRoutingRoute = Schema.Struct({
|
|
29696
29898
|
pathname: ClientPath,
|
|
29697
29899
|
artifact: ArtifactPath
|
|
@@ -29705,25 +29907,64 @@ const SchemaSnapshotField = Schema.Struct({
|
|
|
29705
29907
|
name: Schema.String,
|
|
29706
29908
|
value: ValueDescriptor
|
|
29707
29909
|
});
|
|
29708
|
-
const
|
|
29910
|
+
const SchemaSnapshotIndex = Schema.Struct({
|
|
29911
|
+
name: Schema.String,
|
|
29912
|
+
fields: Schema.Array(Schema.String)
|
|
29913
|
+
});
|
|
29914
|
+
const SchemaSnapshotTableV1 = Schema.Struct({
|
|
29709
29915
|
name: Schema.String,
|
|
29710
29916
|
fields: Schema.Array(SchemaSnapshotField)
|
|
29711
29917
|
});
|
|
29712
|
-
const
|
|
29918
|
+
const SchemaSnapshotV1 = Schema.Struct({
|
|
29713
29919
|
formatVersion: Schema.Literal(1),
|
|
29920
|
+
tables: Schema.Array(SchemaSnapshotTableV1)
|
|
29921
|
+
});
|
|
29922
|
+
const SchemaSnapshotTable = Schema.Struct({
|
|
29923
|
+
name: Schema.String,
|
|
29924
|
+
fields: Schema.Array(SchemaSnapshotField),
|
|
29925
|
+
indexes: Schema.Array(SchemaSnapshotIndex)
|
|
29926
|
+
});
|
|
29927
|
+
const SchemaSnapshotV2 = Schema.Struct({
|
|
29928
|
+
formatVersion: Schema.Literal(2),
|
|
29714
29929
|
tables: Schema.Array(SchemaSnapshotTable)
|
|
29715
29930
|
});
|
|
29931
|
+
const SchemaSnapshot = upgrade(initial(SchemaSnapshotV1), SchemaSnapshotV2, (snapshot) => ({
|
|
29932
|
+
formatVersion: 2,
|
|
29933
|
+
tables: snapshot.tables.map((table) => ({
|
|
29934
|
+
...table,
|
|
29935
|
+
indexes: []
|
|
29936
|
+
}))
|
|
29937
|
+
}));
|
|
29938
|
+
const ServerFunctionArtifactV1 = Schema.Struct({
|
|
29939
|
+
address: FunctionAddress,
|
|
29940
|
+
kind: FunctionKind,
|
|
29941
|
+
bundle: ArtifactReference,
|
|
29942
|
+
sourceMap: ArtifactReference
|
|
29943
|
+
});
|
|
29716
29944
|
const ServerFunctionArtifact = Schema.Struct({
|
|
29717
29945
|
address: FunctionAddress,
|
|
29718
29946
|
kind: FunctionKind,
|
|
29947
|
+
args: Schema.optional(ValueDescriptor),
|
|
29948
|
+
returns: Schema.optional(ValueDescriptor),
|
|
29949
|
+
errors: Schema.optional(ValueDescriptor),
|
|
29719
29950
|
bundle: ArtifactReference,
|
|
29720
29951
|
sourceMap: ArtifactReference
|
|
29721
29952
|
});
|
|
29722
|
-
const
|
|
29953
|
+
const ServerBuildManifestV1 = Schema.Struct({
|
|
29723
29954
|
formatVersion: Schema.Literal(1),
|
|
29724
29955
|
schema: ArtifactReference,
|
|
29956
|
+
functions: Schema.Array(ServerFunctionArtifactV1)
|
|
29957
|
+
});
|
|
29958
|
+
const ServerBuildManifestV2 = Schema.Struct({
|
|
29959
|
+
formatVersion: Schema.Literal(2),
|
|
29960
|
+
schema: ArtifactReference,
|
|
29725
29961
|
functions: Schema.Array(ServerFunctionArtifact)
|
|
29726
29962
|
});
|
|
29963
|
+
const ServerBuildManifest = upgrade(initial(ServerBuildManifestV1), ServerBuildManifestV2, (manifest) => ({
|
|
29964
|
+
formatVersion: 2,
|
|
29965
|
+
schema: manifest.schema,
|
|
29966
|
+
functions: manifest.functions
|
|
29967
|
+
}));
|
|
29727
29968
|
const deploymentInventoryPath = ArtifactPath.make("inventory.json");
|
|
29728
29969
|
const clientManifestPath = ArtifactPath.make("client/manifest.json");
|
|
29729
29970
|
const clientShellPath = ArtifactPath.make("client/shell.html");
|
|
@@ -29839,7 +30080,7 @@ var IdGenerator = class IdGenerator extends Context.Service()("@ignotum/shared/i
|
|
|
29839
30080
|
};
|
|
29840
30081
|
//#endregion
|
|
29841
30082
|
//#region package.json
|
|
29842
|
-
var version = "0.0.
|
|
30083
|
+
var version = "0.0.8";
|
|
29843
30084
|
//#endregion
|
|
29844
30085
|
//#region src/cli/codegen.ts
|
|
29845
30086
|
const generatedHeader = "// Generated by `ignotum codegen`. Do not edit.";
|
|
@@ -30085,12 +30326,12 @@ const controlClientLayer = Layer.effect(ControlClient, Effect.gen(function* () {
|
|
|
30085
30326
|
}));
|
|
30086
30327
|
//#endregion
|
|
30087
30328
|
//#region ../contracts/dist/json.js
|
|
30088
|
-
const encodeScalar = (value) => Schema.decodeSync(Schema.String)(JSON.stringify(value));
|
|
30329
|
+
const encodeScalar$1 = (value) => Schema.decodeSync(Schema.String)(JSON.stringify(value));
|
|
30089
30330
|
const encodeCanonicalJson = (value) => {
|
|
30090
|
-
if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return encodeScalar(value);
|
|
30331
|
+
if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return encodeScalar$1(value);
|
|
30091
30332
|
if (Predicate.isObject(value)) return `{${Array$1.map(Array$1.sort(String$1.Order)(Object.keys(value)), (key) => {
|
|
30092
30333
|
const field = Schema.decodeUnknownSync(Schema.Json)(value[key]);
|
|
30093
|
-
return `${encodeScalar(key)}:${encodeCanonicalJson(field)}`;
|
|
30334
|
+
return `${encodeScalar$1(key)}:${encodeCanonicalJson(field)}`;
|
|
30094
30335
|
}).join(",")}}`;
|
|
30095
30336
|
return `[${Schema.decodeUnknownSync(Schema.Array(Schema.Json))(value).map(encodeCanonicalJson).join(",")}]`;
|
|
30096
30337
|
};
|
|
@@ -30173,6 +30414,58 @@ const documentNotFound = (table, id) => Brand.nominal()({
|
|
|
30173
30414
|
id
|
|
30174
30415
|
});
|
|
30175
30416
|
//#endregion
|
|
30417
|
+
//#region ../contracts/dist/runtime/pagination.js
|
|
30418
|
+
const PaginationCursor = Schema.String.check(Schema.isBase64Url()).pipe(Schema.brand("ignotum/runtime/PaginationCursor"));
|
|
30419
|
+
const PaginationPageSize = Schema.Int.check(Schema.isBetween({
|
|
30420
|
+
minimum: 1,
|
|
30421
|
+
maximum: 1e3
|
|
30422
|
+
})).pipe(Schema.brand("ignotum/runtime/PaginationPageSize"));
|
|
30423
|
+
Schema.Struct({
|
|
30424
|
+
cursor: Schema.NullOr(PaginationCursor),
|
|
30425
|
+
pageSize: PaginationPageSize
|
|
30426
|
+
});
|
|
30427
|
+
const TablePosition = Schema.Struct({
|
|
30428
|
+
type: Schema.Literal("Table"),
|
|
30429
|
+
createdAt: Schema.Int,
|
|
30430
|
+
id: Schema.String
|
|
30431
|
+
});
|
|
30432
|
+
const IndexPosition = Schema.Struct({
|
|
30433
|
+
type: Schema.Literal("Index"),
|
|
30434
|
+
key: Schema.String.check(Schema.isPattern(/^(?:[0-9a-f]{2})*$/)).pipe(Schema.brand("ignotum/runtime/EncodedIndexKey"))
|
|
30435
|
+
});
|
|
30436
|
+
const PaginationPosition = Schema.Union([TablePosition, IndexPosition]);
|
|
30437
|
+
const CursorPayload = initial(Schema.Struct({
|
|
30438
|
+
version: Schema.Literal(1),
|
|
30439
|
+
query: Schema.String,
|
|
30440
|
+
position: PaginationPosition
|
|
30441
|
+
}));
|
|
30442
|
+
const CursorPayloadJson = Schema.fromJsonString(CursorPayload);
|
|
30443
|
+
const paginationQueryIdentity = (tableId, index, lower, upper, order) => encodeCanonicalJson([
|
|
30444
|
+
tableId,
|
|
30445
|
+
index,
|
|
30446
|
+
lower ?? null,
|
|
30447
|
+
upper ?? null,
|
|
30448
|
+
order
|
|
30449
|
+
]);
|
|
30450
|
+
const encodePaginationCursor = (query, position) => PaginationCursor.make(Encoding.encodeBase64Url(Schema.encodeSync(CursorPayloadJson)({
|
|
30451
|
+
version: 1,
|
|
30452
|
+
query,
|
|
30453
|
+
position
|
|
30454
|
+
})));
|
|
30455
|
+
const decodePaginationCursor = (cursor) => Schema.decodeSync(CursorPayloadJson)(Result.getOrThrow(Encoding.decodeBase64UrlString(cursor)));
|
|
30456
|
+
Schema.Union([
|
|
30457
|
+
Schema.String,
|
|
30458
|
+
Schema.Finite,
|
|
30459
|
+
Schema.Boolean
|
|
30460
|
+
]);
|
|
30461
|
+
const schemaIndexLimits = {
|
|
30462
|
+
fieldsPerIndex: 8,
|
|
30463
|
+
indexesPerSchema: 128,
|
|
30464
|
+
indexesPerTable: 16,
|
|
30465
|
+
keyBytes: 4096,
|
|
30466
|
+
nameBytes: 64
|
|
30467
|
+
};
|
|
30468
|
+
//#endregion
|
|
30176
30469
|
//#region ../contracts/dist/runtime/schema.js
|
|
30177
30470
|
const FunctionSchemaTypeId = Symbol.for("ignotum/runtime/schema/FunctionSchema");
|
|
30178
30471
|
const getFunctionSchema = (value) => value[FunctionSchemaTypeId];
|
|
@@ -30472,27 +30765,42 @@ const readArtifactDirectory = Effect.fn("Deployment.readArtifactDirectory")(func
|
|
|
30472
30765
|
const makeSchemaSnapshot = (schema) => {
|
|
30473
30766
|
const definition = schema[SchemaDefinitionTypeId];
|
|
30474
30767
|
return {
|
|
30475
|
-
formatVersion:
|
|
30768
|
+
formatVersion: 2,
|
|
30476
30769
|
tables: Array$1.map(Array$1.sort(String$1.Order)(Object.keys(definition)), (name) => {
|
|
30477
30770
|
const table = definition[name];
|
|
30478
30771
|
if (table === void 0 || table.descriptor.type !== "object") throw new Error(`The table '${name}' does not have a canonical Ignotum descriptor.`);
|
|
30772
|
+
const indexes = Object.values(table.indexes).map((index) => ({
|
|
30773
|
+
name: index.name,
|
|
30774
|
+
fields: globalThis.Array.from(index.fields)
|
|
30775
|
+
})).sort((left, right) => left.name.localeCompare(right.name));
|
|
30479
30776
|
return {
|
|
30480
30777
|
name,
|
|
30481
|
-
fields: table.descriptor.fields
|
|
30778
|
+
fields: table.descriptor.fields,
|
|
30779
|
+
indexes
|
|
30482
30780
|
};
|
|
30483
30781
|
})
|
|
30484
30782
|
};
|
|
30485
30783
|
};
|
|
30486
30784
|
const encodeSchemaSnapshot = (snapshot) => `${encodeCanonical(SchemaSnapshot, snapshot)}\n`;
|
|
30487
|
-
Function$1.dual(2, (previous, next) => previous === void 0 || encodeCanonical(SchemaSnapshot,
|
|
30488
|
-
|
|
30489
|
-
|
|
30490
|
-
|
|
30785
|
+
Function$1.dual(2, (previous, next) => previous === void 0 || encodeCanonical(SchemaSnapshot, {
|
|
30786
|
+
...previous,
|
|
30787
|
+
tables: previous.tables.map((table) => ({
|
|
30788
|
+
...table,
|
|
30789
|
+
indexes: []
|
|
30790
|
+
}))
|
|
30791
|
+
}) === encodeCanonical(SchemaSnapshot, {
|
|
30792
|
+
...next,
|
|
30793
|
+
tables: next.tables.map((table) => ({
|
|
30794
|
+
...table,
|
|
30795
|
+
indexes: []
|
|
30796
|
+
}))
|
|
30797
|
+
}));
|
|
30798
|
+
const AppConfiguration = initial(Schema.Struct({
|
|
30491
30799
|
formatVersion: Schema.Literal(1),
|
|
30492
30800
|
appId: AppId,
|
|
30493
30801
|
slug: AppSlug,
|
|
30494
30802
|
apiUrl: Schema.URLFromString
|
|
30495
|
-
});
|
|
30803
|
+
}));
|
|
30496
30804
|
var AppConfigurationInvalid = class extends Schema.TaggedError()("AppConfigurationInvalid", {
|
|
30497
30805
|
cause: Schema.optional(Schema.Defect()),
|
|
30498
30806
|
message: Schema.String
|
|
@@ -30965,8 +31273,8 @@ var InvalidPublicFile = class extends Schema.TaggedError()("InvalidPublicFile",
|
|
|
30965
31273
|
path: Schema.String
|
|
30966
31274
|
}) {};
|
|
30967
31275
|
const pathSegmentPattern = /^[A-Za-z0-9._~-]+$/;
|
|
30968
|
-
const startsWith = (bytes, signature, offset = 0) => signature.every((byte, index) => bytes[offset + index] === byte);
|
|
30969
|
-
const asciiAt = (bytes, value, offset = 0) => startsWith(bytes, globalThis.Array.from(value, (character) => character.charCodeAt(0)), offset);
|
|
31276
|
+
const startsWith$1 = (bytes, signature, offset = 0) => signature.every((byte, index) => bytes[offset + index] === byte);
|
|
31277
|
+
const asciiAt = (bytes, value, offset = 0) => startsWith$1(bytes, globalThis.Array.from(value, (character) => character.charCodeAt(0)), offset);
|
|
30970
31278
|
const hasValidSignature = (extension, bytes) => {
|
|
30971
31279
|
switch (extension) {
|
|
30972
31280
|
case ".avif": {
|
|
@@ -30976,20 +31284,20 @@ const hasValidSignature = (extension, bytes) => {
|
|
|
30976
31284
|
return false;
|
|
30977
31285
|
}
|
|
30978
31286
|
case ".gif": return asciiAt(bytes, "GIF87a") || asciiAt(bytes, "GIF89a");
|
|
30979
|
-
case ".ico": return startsWith(bytes, [
|
|
31287
|
+
case ".ico": return startsWith$1(bytes, [
|
|
30980
31288
|
0,
|
|
30981
31289
|
0,
|
|
30982
31290
|
1,
|
|
30983
31291
|
0
|
|
30984
31292
|
]);
|
|
30985
31293
|
case ".jpeg":
|
|
30986
|
-
case ".jpg": return startsWith(bytes, [
|
|
31294
|
+
case ".jpg": return startsWith$1(bytes, [
|
|
30987
31295
|
255,
|
|
30988
31296
|
216,
|
|
30989
31297
|
255
|
|
30990
31298
|
]);
|
|
30991
31299
|
case ".pdf": return asciiAt(bytes, "%PDF-");
|
|
30992
|
-
case ".png": return startsWith(bytes, [
|
|
31300
|
+
case ".png": return startsWith$1(bytes, [
|
|
30993
31301
|
137,
|
|
30994
31302
|
80,
|
|
30995
31303
|
78,
|
|
@@ -31163,10 +31471,13 @@ const hostModuleId = "ignotum:host";
|
|
|
31163
31471
|
const encodeJavaScriptString = Schema.encodeSync(Schema.fromJsonString(Schema.String));
|
|
31164
31472
|
const DiscoveredServerFunction = Schema.Struct({
|
|
31165
31473
|
address: FunctionAddress,
|
|
31474
|
+
args: Schema.optional(ValueDescriptor),
|
|
31475
|
+
errors: Schema.optional(ValueDescriptor),
|
|
31166
31476
|
exportName: Schema.String,
|
|
31167
31477
|
kind: Schema.Literals(["Mutation", "Query"]),
|
|
31168
31478
|
moduleName: Schema.String,
|
|
31169
|
-
modulePath: Schema.String
|
|
31479
|
+
modulePath: Schema.String,
|
|
31480
|
+
returns: Schema.optional(ValueDescriptor)
|
|
31170
31481
|
});
|
|
31171
31482
|
const ServerDiscoveryManifest = Schema.Struct({
|
|
31172
31483
|
functions: Schema.Array(DiscoveredServerFunction),
|
|
@@ -31250,12 +31561,50 @@ const discoverModuleFunctions = Effect.fn("Deploy.discoverServerModule")(functio
|
|
|
31250
31561
|
});
|
|
31251
31562
|
continue;
|
|
31252
31563
|
}
|
|
31253
|
-
|
|
31564
|
+
const args = inspected.definition.args === void 0 ? void 0 : {
|
|
31565
|
+
type: "object",
|
|
31566
|
+
fields: descriptorFields(inspected.definition.args)
|
|
31567
|
+
};
|
|
31568
|
+
const returns = inspected.definition.returns === void 0 ? void 0 : getValueDescriptor(inspected.definition.returns);
|
|
31569
|
+
const errors = inspected.definition.errors === void 0 ? void 0 : getValueDescriptor(inspected.definition.errors);
|
|
31570
|
+
if (inspected.definition.returns !== void 0 && returns === void 0 || inspected.definition.errors !== void 0 && errors === void 0) return yield* InvalidServerFunctionExport.make({
|
|
31571
|
+
exportName,
|
|
31572
|
+
message: `${moduleName}.${exportName} uses a validator that was not created by Ignotum values.`,
|
|
31573
|
+
path: modulePath
|
|
31574
|
+
});
|
|
31575
|
+
if (inspected.definition._tag === "Query" && args !== void 0 && descriptorContainsFile(args)) return yield* InvalidServerFunctionExport.make({
|
|
31576
|
+
exportName,
|
|
31577
|
+
message: `${moduleName}.${exportName} cannot accept files in query arguments.`,
|
|
31578
|
+
path: modulePath
|
|
31579
|
+
});
|
|
31580
|
+
if (inspected.definition._tag === "Mutation" && returns !== void 0 && descriptorContainsFile(returns)) return yield* InvalidServerFunctionExport.make({
|
|
31581
|
+
exportName,
|
|
31582
|
+
message: `${moduleName}.${exportName} cannot return files from a mutation.`,
|
|
31583
|
+
path: modulePath
|
|
31584
|
+
});
|
|
31585
|
+
if (errors !== void 0 && descriptorContainsFile(errors)) return yield* InvalidServerFunctionExport.make({
|
|
31586
|
+
exportName,
|
|
31587
|
+
message: `${moduleName}.${exportName} cannot include files in application errors.`,
|
|
31588
|
+
path: modulePath
|
|
31589
|
+
});
|
|
31590
|
+
const discovered = {
|
|
31254
31591
|
address: FunctionAddress.make(`api.${moduleName}.${exportName}`),
|
|
31255
31592
|
exportName,
|
|
31256
31593
|
kind: inspected.definition._tag,
|
|
31257
31594
|
moduleName,
|
|
31258
31595
|
modulePath
|
|
31596
|
+
};
|
|
31597
|
+
const withArgs = args === void 0 ? discovered : {
|
|
31598
|
+
...discovered,
|
|
31599
|
+
args
|
|
31600
|
+
};
|
|
31601
|
+
const withErrors = errors === void 0 ? withArgs : {
|
|
31602
|
+
...withArgs,
|
|
31603
|
+
errors
|
|
31604
|
+
};
|
|
31605
|
+
functions.push(returns === void 0 ? withErrors : {
|
|
31606
|
+
...withErrors,
|
|
31607
|
+
returns
|
|
31259
31608
|
});
|
|
31260
31609
|
}
|
|
31261
31610
|
return Array$1.sortWith(functions, (definition) => definition.address, String$1.Order);
|
|
@@ -31339,6 +31688,7 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
|
|
|
31339
31688
|
appDirectory,
|
|
31340
31689
|
nodeModulesDirectory(),
|
|
31341
31690
|
packageRoot,
|
|
31691
|
+
path.join(packageRoot, "node_modules"),
|
|
31342
31692
|
...workspaceProbes
|
|
31343
31693
|
].map((allowedPath) => `--allow-fs-read=${allowedPath}`);
|
|
31344
31694
|
const exitCode = yield* spawner.exitCode(ChildProcess.make(process$1.execPath, [
|
|
@@ -31445,12 +31795,24 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (a
|
|
|
31445
31795
|
if (!(yield* fileSystem.exists(absoluteSourceMap))) return yield* InvalidBuildOutput.make({ message: `${definition.address} did not emit a source map.` });
|
|
31446
31796
|
const bundlePath = normalizePath(path.join("server", relativeFile));
|
|
31447
31797
|
const sourceMapPath = normalizePath(path.join("server", relativeSourceMap));
|
|
31448
|
-
|
|
31798
|
+
const artifact = {
|
|
31449
31799
|
address: definition.address,
|
|
31450
31800
|
kind: definition.kind,
|
|
31451
31801
|
bundle: yield* artifactReference(bundlePath, yield* fileSystem.readFile(absoluteFile)),
|
|
31452
31802
|
sourceMap: yield* artifactReference(sourceMapPath, yield* fileSystem.readFile(absoluteSourceMap))
|
|
31453
31803
|
};
|
|
31804
|
+
const withArgs = definition.args === void 0 ? artifact : {
|
|
31805
|
+
...artifact,
|
|
31806
|
+
args: definition.args
|
|
31807
|
+
};
|
|
31808
|
+
const withErrors = definition.errors === void 0 ? withArgs : {
|
|
31809
|
+
...withArgs,
|
|
31810
|
+
errors: definition.errors
|
|
31811
|
+
};
|
|
31812
|
+
return definition.returns === void 0 ? withErrors : {
|
|
31813
|
+
...withErrors,
|
|
31814
|
+
returns: definition.returns
|
|
31815
|
+
};
|
|
31454
31816
|
});
|
|
31455
31817
|
const buildServer = Effect.fn("Deploy.buildServer")(function* (appDirectory, outputDirectory, functionModules) {
|
|
31456
31818
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
@@ -31463,7 +31825,7 @@ const buildServer = Effect.fn("Deploy.buildServer")(function* (appDirectory, out
|
|
|
31463
31825
|
yield* fileSystem.writeFile(path.join(outputDirectory, "schema.json"), snapshotBytes);
|
|
31464
31826
|
const functions = yield* Effect.forEach(discovered.functions, (definition) => buildServerFunction(appDirectory, outputDirectory, conditions, definition), { concurrency: 4 });
|
|
31465
31827
|
const manifest = {
|
|
31466
|
-
formatVersion:
|
|
31828
|
+
formatVersion: 2,
|
|
31467
31829
|
schema: yield* artifactReference(schemaSnapshotPath, snapshotBytes),
|
|
31468
31830
|
functions
|
|
31469
31831
|
};
|
|
@@ -31848,7 +32210,7 @@ const make$2 = (options) => Effect$1.gen(function* () {
|
|
|
31848
32210
|
return transformRows ? Effect$1.map(effect, transformRows) : effect;
|
|
31849
32211
|
},
|
|
31850
32212
|
executeStream(_sql, _params) {
|
|
31851
|
-
return Stream.die("executeStream not implemented");
|
|
32213
|
+
return Stream$1.die("executeStream not implemented");
|
|
31852
32214
|
},
|
|
31853
32215
|
backup(destination) {
|
|
31854
32216
|
return Effect$1.suspend(() => {
|
|
@@ -31912,6 +32274,113 @@ const sqliteCauseWithErrno = (cause) => {
|
|
|
31912
32274
|
return Object.assign(cause, { errno: errcode });
|
|
31913
32275
|
};
|
|
31914
32276
|
//#endregion
|
|
32277
|
+
//#region ../contracts/dist/runtime/index.js
|
|
32278
|
+
const IndexIdentity = Schema.String.pipe(Schema.brand("ignotum/runtime/IndexIdentity"));
|
|
32279
|
+
const EncodedIndexKey = Schema.String.check(Schema.isPattern(/^(?:[0-9a-f]{2})*$/)).pipe(Schema.brand("ignotum/runtime/EncodedIndexKey"));
|
|
32280
|
+
const IndexScalarValue = Schema.Union([
|
|
32281
|
+
Schema.Boolean,
|
|
32282
|
+
Schema.Finite,
|
|
32283
|
+
Schema.String
|
|
32284
|
+
]);
|
|
32285
|
+
const scalarValueKind = (value) => Schema.is(Schema.Boolean)(value) ? "boolean" : Schema.is(Schema.Finite)(value) ? "number" : "string";
|
|
32286
|
+
const encodeString = (value) => {
|
|
32287
|
+
const bytes = [];
|
|
32288
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
32289
|
+
const unit = value.charCodeAt(index);
|
|
32290
|
+
bytes.push(1, unit >>> 8, unit & 255);
|
|
32291
|
+
}
|
|
32292
|
+
bytes.push(0);
|
|
32293
|
+
return Array.from(bytes);
|
|
32294
|
+
};
|
|
32295
|
+
const encodeNumber = (input) => {
|
|
32296
|
+
const value = Object.is(input, -0) ? 0 : input;
|
|
32297
|
+
if (!Number.isFinite(value)) throw new Error("Index numbers must be finite.");
|
|
32298
|
+
const bytes = /* @__PURE__ */ new Uint8Array(8);
|
|
32299
|
+
new DataView(bytes.buffer).setFloat64(0, value, false);
|
|
32300
|
+
if ((bytes[0] ?? 0) >= 128) for (let index = 0; index < bytes.length; index += 1) bytes[index] = 255 - bytes[index];
|
|
32301
|
+
else bytes[0] = (bytes[0] ?? 0) ^ 128;
|
|
32302
|
+
return Array.from(bytes);
|
|
32303
|
+
};
|
|
32304
|
+
const scalarKind = (descriptor) => {
|
|
32305
|
+
switch (descriptor.type) {
|
|
32306
|
+
case "boolean": return "boolean";
|
|
32307
|
+
case "date":
|
|
32308
|
+
case "integer":
|
|
32309
|
+
case "number": return "number";
|
|
32310
|
+
case "id":
|
|
32311
|
+
case "string": return "string";
|
|
32312
|
+
case "literal": return scalarValueKind(descriptor.value);
|
|
32313
|
+
case "literals": {
|
|
32314
|
+
const first = descriptor.values[0];
|
|
32315
|
+
if (first === void 0) throw new Error("Indexed literal sets cannot be empty.");
|
|
32316
|
+
const kind = scalarValueKind(first);
|
|
32317
|
+
if (descriptor.values.some((value) => scalarValueKind(value) !== kind)) throw new Error("Indexed literal sets cannot mix value types.");
|
|
32318
|
+
return kind;
|
|
32319
|
+
}
|
|
32320
|
+
default: throw new Error(`Values of type '${descriptor.type}' cannot be indexed.`);
|
|
32321
|
+
}
|
|
32322
|
+
};
|
|
32323
|
+
const encodeScalar = (descriptor, value) => {
|
|
32324
|
+
switch (scalarKind(descriptor)) {
|
|
32325
|
+
case "boolean":
|
|
32326
|
+
if (!Schema.is(Schema.Boolean)(value)) throw new Error("Expected an indexed boolean value.");
|
|
32327
|
+
return [value ? 1 : 0];
|
|
32328
|
+
case "number":
|
|
32329
|
+
if (!Schema.is(Schema.Finite)(value)) throw new Error("Expected an indexed number value.");
|
|
32330
|
+
return encodeNumber(value);
|
|
32331
|
+
case "string":
|
|
32332
|
+
if (!Schema.is(Schema.String)(value)) throw new Error("Expected an indexed string value.");
|
|
32333
|
+
return encodeString(value);
|
|
32334
|
+
}
|
|
32335
|
+
};
|
|
32336
|
+
const encodeBytes = (bytes) => EncodedIndexKey.make(bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(""));
|
|
32337
|
+
const encodeIndexPrefix = (descriptors, values) => {
|
|
32338
|
+
if (descriptors.length !== values.length) throw new Error("Index descriptors and values must have the same length.");
|
|
32339
|
+
const bytes = descriptors.flatMap((descriptor, index) => encodeScalar(descriptor, values[index]));
|
|
32340
|
+
if (bytes.length > schemaIndexLimits.keyBytes) throw new Error(`Index keys cannot exceed ${schemaIndexLimits.keyBytes} bytes.`);
|
|
32341
|
+
return encodeBytes(bytes);
|
|
32342
|
+
};
|
|
32343
|
+
const encodeIndexKey = (descriptors, values, createdAt, id) => {
|
|
32344
|
+
const bytes = [
|
|
32345
|
+
...descriptors.flatMap((descriptor, index) => encodeScalar(descriptor, values[index])),
|
|
32346
|
+
...encodeNumber(createdAt),
|
|
32347
|
+
...encodeString(id)
|
|
32348
|
+
];
|
|
32349
|
+
if (bytes.length > schemaIndexLimits.keyBytes) throw new Error(`Index keys cannot exceed ${schemaIndexLimits.keyBytes} bytes.`);
|
|
32350
|
+
return encodeBytes(bytes);
|
|
32351
|
+
};
|
|
32352
|
+
const successorIndexPrefix = (prefix) => {
|
|
32353
|
+
const bytes = Array.from({ length: prefix.length / 2 }, (_, index) => Number.parseInt(prefix.slice(index * 2, index * 2 + 2), 16));
|
|
32354
|
+
for (let index = bytes.length - 1; index >= 0; index -= 1) {
|
|
32355
|
+
const byte = bytes[index];
|
|
32356
|
+
if (byte === void 0 || byte === 255) continue;
|
|
32357
|
+
bytes[index] = byte + 1;
|
|
32358
|
+
return encodeBytes(bytes.slice(0, index + 1));
|
|
32359
|
+
}
|
|
32360
|
+
};
|
|
32361
|
+
const encodeIndexRange = (descriptors, equal, lower, upper) => {
|
|
32362
|
+
const equalDescriptors = descriptors.slice(0, equal.length);
|
|
32363
|
+
const prefix = encodeIndexPrefix(equalDescriptors, equal);
|
|
32364
|
+
const rangeDescriptor = descriptors[equal.length];
|
|
32365
|
+
if ((lower !== void 0 || upper !== void 0) && rangeDescriptor === void 0) throw new Error("An index range exceeded the index fields.");
|
|
32366
|
+
const encodedLower = lower === void 0 ? prefix.length === 0 ? void 0 : prefix : EncodedIndexKey.make(`${prefix}${encodeIndexPrefix([rangeDescriptor], [lower.value])}`);
|
|
32367
|
+
const encodedUpper = upper === void 0 ? prefix.length === 0 ? void 0 : successorIndexPrefix(prefix) : EncodedIndexKey.make(`${prefix}${encodeIndexPrefix([rangeDescriptor], [upper.value])}`);
|
|
32368
|
+
const finalLower = encodedLower !== void 0 && lower !== void 0 && !lower.inclusive ? successorIndexPrefix(encodedLower) : encodedLower;
|
|
32369
|
+
const finalUpper = encodedUpper !== void 0 && upper !== void 0 && upper.inclusive ? successorIndexPrefix(encodedUpper) : encodedUpper;
|
|
32370
|
+
if (finalLower === void 0 && finalUpper === void 0) return {};
|
|
32371
|
+
if (finalLower === void 0) return { upper: finalUpper };
|
|
32372
|
+
if (finalUpper === void 0) return { lower: finalLower };
|
|
32373
|
+
return {
|
|
32374
|
+
lower: finalLower,
|
|
32375
|
+
upper: finalUpper
|
|
32376
|
+
};
|
|
32377
|
+
};
|
|
32378
|
+
const indexIdentity = (tableId, name, fields) => IndexIdentity.make(encodeCanonicalJson([
|
|
32379
|
+
tableId,
|
|
32380
|
+
name,
|
|
32381
|
+
fields
|
|
32382
|
+
]));
|
|
32383
|
+
//#endregion
|
|
31915
32384
|
//#region ../contracts/dist/runtime/hosted.js
|
|
31916
32385
|
const QueryKey = Schema.String.pipe(Schema.brand("ignotum/hosted/QueryKey"));
|
|
31917
32386
|
const TableDependency = Schema.Struct({
|
|
@@ -31923,20 +32392,71 @@ const DocumentDependency = Schema.Struct({
|
|
|
31923
32392
|
tableId: TableId,
|
|
31924
32393
|
id: GeneratedId
|
|
31925
32394
|
});
|
|
31926
|
-
const
|
|
31927
|
-
|
|
31928
|
-
|
|
32395
|
+
const IndexRangeDependency = Schema.Struct({
|
|
32396
|
+
type: Schema.Literal("IndexRange"),
|
|
32397
|
+
tableId: TableId,
|
|
32398
|
+
index: IndexIdentity,
|
|
32399
|
+
lower: Schema.optional(EncodedIndexKey),
|
|
32400
|
+
upper: Schema.optional(EncodedIndexKey)
|
|
32401
|
+
});
|
|
32402
|
+
const IndexPointInvalidation = Schema.Struct({
|
|
32403
|
+
type: Schema.Literal("IndexPoint"),
|
|
32404
|
+
tableId: TableId,
|
|
32405
|
+
index: IndexIdentity,
|
|
32406
|
+
key: EncodedIndexKey
|
|
32407
|
+
});
|
|
32408
|
+
const ReadDependency = Schema.Union([
|
|
32409
|
+
TableDependency,
|
|
32410
|
+
DocumentDependency,
|
|
32411
|
+
IndexRangeDependency
|
|
32412
|
+
]);
|
|
32413
|
+
const WriteInvalidation = Schema.Union([
|
|
32414
|
+
TableDependency,
|
|
32415
|
+
DocumentDependency,
|
|
32416
|
+
IndexPointInvalidation
|
|
32417
|
+
]);
|
|
32418
|
+
Schema.Union([ReadDependency, IndexPointInvalidation]);
|
|
32419
|
+
const DependencySet = Schema.Array(ReadDependency);
|
|
32420
|
+
const InvalidationSet = Schema.Array(WriteInvalidation);
|
|
31929
32421
|
const RuntimeQueryResult = Schema.Struct({
|
|
31930
32422
|
type: Schema.Literal("Query"),
|
|
31931
32423
|
result: WireResult,
|
|
31932
32424
|
dependencies: DependencySet,
|
|
31933
|
-
observedRevision: AppStateRevision
|
|
32425
|
+
observedRevision: AppStateRevision,
|
|
32426
|
+
files: Schema.optional(Schema.Array(RuntimeFileOccurrence))
|
|
32427
|
+
});
|
|
32428
|
+
const FileReferenceAdded = Schema.Struct({
|
|
32429
|
+
type: Schema.Literal("AddReference"),
|
|
32430
|
+
file: RuntimeFileMetadata,
|
|
32431
|
+
tableId: TableId,
|
|
32432
|
+
documentId: GeneratedId
|
|
31934
32433
|
});
|
|
32434
|
+
const FileReferenceRemoved = Schema.Struct({
|
|
32435
|
+
type: Schema.Literal("RemoveReference"),
|
|
32436
|
+
fileId: FileId,
|
|
32437
|
+
tableId: TableId,
|
|
32438
|
+
documentId: GeneratedId
|
|
32439
|
+
});
|
|
32440
|
+
const FileReferenceEffect = Schema.Union([FileReferenceAdded, FileReferenceRemoved]);
|
|
32441
|
+
const FileEffectBatch = Schema.Struct({
|
|
32442
|
+
id: InvocationId,
|
|
32443
|
+
committedRevision: AppStateRevision,
|
|
32444
|
+
effects: Schema.Array(FileReferenceEffect)
|
|
32445
|
+
});
|
|
32446
|
+
Schema.Struct({ appId: AppId });
|
|
32447
|
+
Schema.Struct({ batches: Schema.Array(FileEffectBatch) });
|
|
32448
|
+
Schema.Struct({
|
|
32449
|
+
appId: AppId,
|
|
32450
|
+
invocationId: InvocationId,
|
|
32451
|
+
acknowledgedAt: Schema.Int
|
|
32452
|
+
});
|
|
32453
|
+
Schema.Struct({ completed: Schema.Literal(true) });
|
|
31935
32454
|
const RuntimeMutationSuccess = Schema.Struct({
|
|
31936
32455
|
type: Schema.Literal("Mutation"),
|
|
31937
32456
|
result: WireSuccess,
|
|
31938
32457
|
invalidations: InvalidationSet,
|
|
31939
|
-
committedRevision: AppStateRevision
|
|
32458
|
+
committedRevision: AppStateRevision,
|
|
32459
|
+
fileEffects: Schema.optional(FileEffectBatch)
|
|
31940
32460
|
});
|
|
31941
32461
|
const RuntimeMutationFailure = Schema.Struct({
|
|
31942
32462
|
type: Schema.Literal("Mutation"),
|
|
@@ -31959,7 +32479,8 @@ const QueryInvocation = Schema.Struct({
|
|
|
31959
32479
|
const MutationInvocation = Schema.Struct({
|
|
31960
32480
|
...InvocationBase,
|
|
31961
32481
|
type: Schema.Literal("Mutation"),
|
|
31962
|
-
invocationId: InvocationId
|
|
32482
|
+
invocationId: InvocationId,
|
|
32483
|
+
files: Schema.optional(Schema.Array(RuntimeFileOccurrence))
|
|
31963
32484
|
});
|
|
31964
32485
|
Schema.Union([QueryInvocation, MutationInvocation]);
|
|
31965
32486
|
Schema.Struct({
|
|
@@ -31997,10 +32518,19 @@ Schema.Union([
|
|
|
31997
32518
|
RuntimeInvocationRejected,
|
|
31998
32519
|
RuntimeInvocationUnavailable
|
|
31999
32520
|
]);
|
|
32000
|
-
const dependencyKey = (dependency) => dependency.type === "Table" ? encodeCanonicalJson([dependency.type, dependency.tableId]) : encodeCanonicalJson([
|
|
32521
|
+
const dependencyKey = (dependency) => dependency.type === "Table" ? encodeCanonicalJson([dependency.type, dependency.tableId]) : dependency.type === "Document" ? encodeCanonicalJson([
|
|
32001
32522
|
dependency.type,
|
|
32002
32523
|
dependency.tableId,
|
|
32003
32524
|
dependency.id
|
|
32525
|
+
]) : dependency.type === "IndexPoint" ? encodeCanonicalJson([
|
|
32526
|
+
dependency.type,
|
|
32527
|
+
dependency.index,
|
|
32528
|
+
dependency.key
|
|
32529
|
+
]) : encodeCanonicalJson([
|
|
32530
|
+
dependency.type,
|
|
32531
|
+
dependency.index,
|
|
32532
|
+
dependency.lower ?? null,
|
|
32533
|
+
dependency.upper ?? null
|
|
32004
32534
|
]);
|
|
32005
32535
|
const canonicalQueryKey = (deploymentId, functionAddress, args) => QueryKey.make(encodeCanonicalJson([
|
|
32006
32536
|
deploymentId,
|
|
@@ -32022,14 +32552,98 @@ const documentDependency = (tableId, id) => ({
|
|
|
32022
32552
|
tableId,
|
|
32023
32553
|
id
|
|
32024
32554
|
});
|
|
32555
|
+
const indexRangeDependency = (tableId, index, lower, upper) => ({
|
|
32556
|
+
type: "IndexRange",
|
|
32557
|
+
tableId,
|
|
32558
|
+
index,
|
|
32559
|
+
lower,
|
|
32560
|
+
upper
|
|
32561
|
+
});
|
|
32562
|
+
const indexPointInvalidation = (tableId, index, key) => ({
|
|
32563
|
+
type: "IndexPoint",
|
|
32564
|
+
tableId,
|
|
32565
|
+
index,
|
|
32566
|
+
key
|
|
32567
|
+
});
|
|
32025
32568
|
//#endregion
|
|
32026
32569
|
//#region ../runtime/dist/functions.js
|
|
32027
32570
|
var FunctionRuntime = class extends Context.Service()("@ignotum/runtime/functions/FunctionRuntime") {};
|
|
32028
32571
|
//#endregion
|
|
32572
|
+
//#region ../contracts/dist/runtime/transport.js
|
|
32573
|
+
const runtimeInvocationPath = "/v1/invoke";
|
|
32574
|
+
const runtimeRevisionPath = "/v1/revision";
|
|
32575
|
+
const runtimeIndexPreparePath = "/v1/indexes/prepare";
|
|
32576
|
+
const runtimeIndexCommitPath = "/v1/indexes/commit";
|
|
32577
|
+
const runtimeFileEffectsPath = "/v1/files/effects";
|
|
32578
|
+
const runtimeFileEffectsAcknowledgePath = "/v1/files/effects/acknowledge";
|
|
32579
|
+
const ignotumPathPrefix$1 = "/_ignotum";
|
|
32580
|
+
const appSyncPath = `${ignotumPathPrefix$1}/v1/sync`;
|
|
32581
|
+
const fileUploadUrlPrefix = `${ignotumPathPrefix$1}/v1/files/upload/`;
|
|
32582
|
+
const fileGrantUrlPrefix = `${ignotumPathPrefix$1}/v1/files/grant/`;
|
|
32583
|
+
const RuntimeRequestPath = Schema.Literals([
|
|
32584
|
+
runtimeInvocationPath,
|
|
32585
|
+
runtimeRevisionPath,
|
|
32586
|
+
runtimeIndexPreparePath,
|
|
32587
|
+
runtimeIndexCommitPath,
|
|
32588
|
+
runtimeFileEffectsPath,
|
|
32589
|
+
runtimeFileEffectsAcknowledgePath
|
|
32590
|
+
]);
|
|
32591
|
+
const RuntimeRequestTimestamp = Schema.FiniteFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ignotum/runtime/RequestTimestamp"));
|
|
32592
|
+
const RuntimeRequestSignature = Sha256.pipe(Schema.brand("ignotum/runtime/RequestSignature"));
|
|
32593
|
+
Schema.Struct({
|
|
32594
|
+
appId: AppId,
|
|
32595
|
+
deploymentId: Schema.optional(DeploymentId),
|
|
32596
|
+
nonce: RuntimeRequestNonce,
|
|
32597
|
+
timestamp: RuntimeRequestTimestamp,
|
|
32598
|
+
signature: RuntimeRequestSignature
|
|
32599
|
+
});
|
|
32600
|
+
Schema.Struct({
|
|
32601
|
+
method: Schema.Literal("POST"),
|
|
32602
|
+
path: RuntimeRequestPath,
|
|
32603
|
+
timestamp: RuntimeRequestTimestamp,
|
|
32604
|
+
nonce: RuntimeRequestNonce,
|
|
32605
|
+
appId: AppId,
|
|
32606
|
+
deploymentId: Schema.optional(DeploymentId),
|
|
32607
|
+
bodySha256: Sha256
|
|
32608
|
+
});
|
|
32609
|
+
Schema.Struct({
|
|
32610
|
+
appId: AppId,
|
|
32611
|
+
deploymentId: DeploymentId
|
|
32612
|
+
});
|
|
32613
|
+
Schema.Struct({ completed: Schema.Literal(true) });
|
|
32614
|
+
const RuntimeErrorCode = Schema.Literals([
|
|
32615
|
+
"InvalidRequest",
|
|
32616
|
+
"InvalidSignature",
|
|
32617
|
+
"RequestExpired",
|
|
32618
|
+
"RequestReplay",
|
|
32619
|
+
"InvocationRejected",
|
|
32620
|
+
"RuntimeUnavailable"
|
|
32621
|
+
]);
|
|
32622
|
+
Schema.Struct({
|
|
32623
|
+
code: RuntimeErrorCode,
|
|
32624
|
+
message: Schema.String,
|
|
32625
|
+
rejectionCode: Schema.optional(RuntimeInvocationRejectionCode)
|
|
32626
|
+
});
|
|
32627
|
+
//#endregion
|
|
32628
|
+
//#region ../shared/dist/http-body.js
|
|
32629
|
+
const joinChunks = (chunks) => {
|
|
32630
|
+
const byteLength = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
32631
|
+
const bytes = new Uint8Array(byteLength);
|
|
32632
|
+
let offset = 0;
|
|
32633
|
+
for (const chunk of chunks) {
|
|
32634
|
+
bytes.set(chunk, offset);
|
|
32635
|
+
offset += chunk.byteLength;
|
|
32636
|
+
}
|
|
32637
|
+
return bytes;
|
|
32638
|
+
};
|
|
32639
|
+
const collectStreamBytes = Function$1.dual(3, (stream, maxBytes, onLimitReached) => Stream.mapError(stream, (error) => error).pipe(Stream.limitBytes(maxBytes, () => Stream.fail(onLimitReached())), Stream.runCollect, Effect.map(joinChunks)));
|
|
32640
|
+
//#endregion
|
|
32029
32641
|
//#region ../runtime/dist/sync.js
|
|
32030
32642
|
const makeDependencyIndex = () => {
|
|
32031
32643
|
const dependenciesByQuery = MutableHashMap.empty();
|
|
32032
32644
|
const queriesByDependency = MutableHashMap.empty();
|
|
32645
|
+
const rangesByQuery = MutableHashMap.empty();
|
|
32646
|
+
const queriesByIndex = MutableHashMap.empty();
|
|
32033
32647
|
const remove = (queryId) => {
|
|
32034
32648
|
const dependencies = Option.getOrUndefined(MutableHashMap.get(dependenciesByQuery, queryId));
|
|
32035
32649
|
if (dependencies === void 0) return;
|
|
@@ -32039,12 +32653,23 @@ const makeDependencyIndex = () => {
|
|
|
32039
32653
|
MutableHashSet.remove(queries, queryId);
|
|
32040
32654
|
if (MutableHashSet.size(queries) === 0) MutableHashMap.remove(queriesByDependency, key);
|
|
32041
32655
|
}
|
|
32656
|
+
const ranges = Option.getOrElse(MutableHashMap.get(rangesByQuery, queryId), () => []);
|
|
32657
|
+
for (const range of ranges) {
|
|
32658
|
+
const queries = Option.getOrUndefined(MutableHashMap.get(queriesByIndex, range.index));
|
|
32659
|
+
if (queries === void 0) continue;
|
|
32660
|
+
MutableHashSet.remove(queries, queryId);
|
|
32661
|
+
if (MutableHashSet.size(queries) === 0) MutableHashMap.remove(queriesByIndex, range.index);
|
|
32662
|
+
}
|
|
32663
|
+
MutableHashMap.remove(rangesByQuery, queryId);
|
|
32042
32664
|
MutableHashMap.remove(dependenciesByQuery, queryId);
|
|
32043
32665
|
};
|
|
32044
32666
|
const record = (queryId, dependencies) => {
|
|
32045
32667
|
remove(queryId);
|
|
32046
|
-
const
|
|
32668
|
+
const exact = dependencies.filter((dependency) => dependency.type !== "IndexRange");
|
|
32669
|
+
const ranges = dependencies.filter((dependency) => dependency.type === "IndexRange");
|
|
32670
|
+
const keys = MutableHashSet.fromIterable(exact.map(dependencyKey));
|
|
32047
32671
|
MutableHashMap.set(dependenciesByQuery, queryId, keys);
|
|
32672
|
+
MutableHashMap.set(rangesByQuery, queryId, ranges);
|
|
32048
32673
|
for (const key of keys) {
|
|
32049
32674
|
const current = Option.getOrElse(MutableHashMap.get(queriesByDependency, key), () => {
|
|
32050
32675
|
const created = MutableHashSet.empty();
|
|
@@ -32053,10 +32678,24 @@ const makeDependencyIndex = () => {
|
|
|
32053
32678
|
});
|
|
32054
32679
|
MutableHashSet.add(current, queryId);
|
|
32055
32680
|
}
|
|
32681
|
+
for (const range of ranges) {
|
|
32682
|
+
const current = Option.getOrElse(MutableHashMap.get(queriesByIndex, range.index), () => {
|
|
32683
|
+
const created = MutableHashSet.empty();
|
|
32684
|
+
MutableHashMap.set(queriesByIndex, range.index, created);
|
|
32685
|
+
return created;
|
|
32686
|
+
});
|
|
32687
|
+
MutableHashSet.add(current, queryId);
|
|
32688
|
+
}
|
|
32056
32689
|
};
|
|
32057
32690
|
const affected = (invalidations) => {
|
|
32058
32691
|
const queries = MutableHashSet.empty();
|
|
32059
32692
|
for (const invalidation of invalidations) {
|
|
32693
|
+
if (invalidation.type === "IndexPoint") {
|
|
32694
|
+
const indexed = Option.getOrUndefined(MutableHashMap.get(queriesByIndex, invalidation.index));
|
|
32695
|
+
if (indexed === void 0) continue;
|
|
32696
|
+
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);
|
|
32697
|
+
continue;
|
|
32698
|
+
}
|
|
32060
32699
|
const current = Option.getOrUndefined(MutableHashMap.get(queriesByDependency, dependencyKey(invalidation)));
|
|
32061
32700
|
if (current !== void 0) for (const queryId of current) MutableHashSet.add(queries, queryId);
|
|
32062
32701
|
}
|
|
@@ -32069,43 +32708,6 @@ const makeDependencyIndex = () => {
|
|
|
32069
32708
|
};
|
|
32070
32709
|
};
|
|
32071
32710
|
var QueryInvalidation = class extends Context.Service()("@ignotum/runtime/sync/QueryInvalidation") {};
|
|
32072
|
-
//#endregion
|
|
32073
|
-
//#region ../contracts/dist/runtime/transport.js
|
|
32074
|
-
const runtimeInvocationPath = "/v1/invoke";
|
|
32075
|
-
const runtimeRevisionPath = "/v1/revision";
|
|
32076
|
-
const appSyncPath = `/_ignotum/v1/sync`;
|
|
32077
|
-
const RuntimeRequestPath = Schema.Literals([runtimeInvocationPath, runtimeRevisionPath]);
|
|
32078
|
-
const RuntimeRequestTimestamp = Schema.FiniteFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ignotum/runtime/RequestTimestamp"));
|
|
32079
|
-
const RuntimeRequestSignature = Sha256.pipe(Schema.brand("ignotum/runtime/RequestSignature"));
|
|
32080
|
-
Schema.Struct({
|
|
32081
|
-
appId: AppId,
|
|
32082
|
-
deploymentId: Schema.optional(DeploymentId),
|
|
32083
|
-
nonce: RuntimeRequestNonce,
|
|
32084
|
-
timestamp: RuntimeRequestTimestamp,
|
|
32085
|
-
signature: RuntimeRequestSignature
|
|
32086
|
-
});
|
|
32087
|
-
Schema.Struct({
|
|
32088
|
-
method: Schema.Literal("POST"),
|
|
32089
|
-
path: RuntimeRequestPath,
|
|
32090
|
-
timestamp: RuntimeRequestTimestamp,
|
|
32091
|
-
nonce: RuntimeRequestNonce,
|
|
32092
|
-
appId: AppId,
|
|
32093
|
-
deploymentId: Schema.optional(DeploymentId),
|
|
32094
|
-
bodySha256: Sha256
|
|
32095
|
-
});
|
|
32096
|
-
const RuntimeErrorCode = Schema.Literals([
|
|
32097
|
-
"InvalidRequest",
|
|
32098
|
-
"InvalidSignature",
|
|
32099
|
-
"RequestExpired",
|
|
32100
|
-
"RequestReplay",
|
|
32101
|
-
"InvocationRejected",
|
|
32102
|
-
"RuntimeUnavailable"
|
|
32103
|
-
]);
|
|
32104
|
-
Schema.Struct({
|
|
32105
|
-
code: RuntimeErrorCode,
|
|
32106
|
-
message: Schema.String,
|
|
32107
|
-
rejectionCode: Schema.optional(RuntimeInvocationRejectionCode)
|
|
32108
|
-
});
|
|
32109
32711
|
const syncPath = appSyncPath;
|
|
32110
32712
|
const isIgnotumPath = (pathname) => pathname === "/_ignotum" || pathname.startsWith(`/_ignotum/`);
|
|
32111
32713
|
//#endregion
|
|
@@ -32153,7 +32755,50 @@ const initialDevelopmentSchema = Effect.gen(function* () {
|
|
|
32153
32755
|
) STRICT
|
|
32154
32756
|
`;
|
|
32155
32757
|
});
|
|
32156
|
-
const
|
|
32758
|
+
const addDevelopmentIndexes = Effect.gen(function* () {
|
|
32759
|
+
const sql = yield* Client.SqlClient;
|
|
32760
|
+
yield* sql`
|
|
32761
|
+
CREATE TABLE indexes (
|
|
32762
|
+
id INTEGER PRIMARY KEY NOT NULL,
|
|
32763
|
+
tableId TEXT NOT NULL REFERENCES tables(id) ON DELETE CASCADE,
|
|
32764
|
+
identity TEXT NOT NULL UNIQUE,
|
|
32765
|
+
name TEXT NOT NULL,
|
|
32766
|
+
fields TEXT NOT NULL CHECK (json_valid(fields))
|
|
32767
|
+
) STRICT
|
|
32768
|
+
`;
|
|
32769
|
+
yield* sql`
|
|
32770
|
+
CREATE TABLE indexEntries (
|
|
32771
|
+
indexId INTEGER NOT NULL REFERENCES indexes(id) ON DELETE CASCADE,
|
|
32772
|
+
tableId TEXT NOT NULL,
|
|
32773
|
+
documentId TEXT NOT NULL,
|
|
32774
|
+
key TEXT NOT NULL,
|
|
32775
|
+
PRIMARY KEY (indexId, documentId),
|
|
32776
|
+
FOREIGN KEY (tableId, documentId)
|
|
32777
|
+
REFERENCES documents(tableId, id)
|
|
32778
|
+
ON DELETE CASCADE
|
|
32779
|
+
) STRICT
|
|
32780
|
+
`;
|
|
32781
|
+
yield* sql`
|
|
32782
|
+
CREATE UNIQUE INDEX index_entries_by_key
|
|
32783
|
+
ON indexEntries (indexId, key)
|
|
32784
|
+
`;
|
|
32785
|
+
});
|
|
32786
|
+
const addApplicationFiles = Effect.gen(function* () {
|
|
32787
|
+
yield* (yield* Client.SqlClient)`
|
|
32788
|
+
CREATE TABLE applicationFiles (
|
|
32789
|
+
fileId TEXT PRIMARY KEY NOT NULL,
|
|
32790
|
+
name TEXT NOT NULL,
|
|
32791
|
+
format TEXT NOT NULL CHECK (format IN ('jpeg', 'png', 'webp', 'avif', 'gif')),
|
|
32792
|
+
size INTEGER NOT NULL CHECK (size >= 0),
|
|
32793
|
+
ready INTEGER NOT NULL DEFAULT 0 CHECK (ready IN (0, 1))
|
|
32794
|
+
) STRICT
|
|
32795
|
+
`;
|
|
32796
|
+
});
|
|
32797
|
+
const developmentMigrationLoader = Migrator.fromRecord({
|
|
32798
|
+
"0001_initial_development_schema": initialDevelopmentSchema,
|
|
32799
|
+
"0002_application_indexes": addDevelopmentIndexes,
|
|
32800
|
+
"0003_application_files": addApplicationFiles
|
|
32801
|
+
});
|
|
32157
32802
|
var DevelopmentDatabase = class DevelopmentDatabase extends Context.Service()("ignotum/dev-runtime/migrations/DevelopmentDatabase") {
|
|
32158
32803
|
static layer = Layer.effect(DevelopmentDatabase, Migrator.make({})({
|
|
32159
32804
|
loader: developmentMigrationLoader,
|
|
@@ -32168,6 +32813,16 @@ const StoredDocument = Schema.Struct({
|
|
|
32168
32813
|
updatedAt: Schema.Int,
|
|
32169
32814
|
fields: Schema.String
|
|
32170
32815
|
});
|
|
32816
|
+
const IndexedStoredDocument = Schema.Struct({
|
|
32817
|
+
...StoredDocument.fields,
|
|
32818
|
+
key: Schema.String
|
|
32819
|
+
});
|
|
32820
|
+
const StoredIndex = Schema.Struct({
|
|
32821
|
+
id: Schema.Natural,
|
|
32822
|
+
identity: Schema.String,
|
|
32823
|
+
name: Schema.String,
|
|
32824
|
+
fields: Schema.String
|
|
32825
|
+
});
|
|
32171
32826
|
const StoredTable = Schema.Struct({
|
|
32172
32827
|
id: TableId,
|
|
32173
32828
|
name: Schema.String
|
|
@@ -32240,6 +32895,18 @@ const toRuntimeDocument = Effect.fn("LocalDatabase.toRuntimeDocument")(function*
|
|
|
32240
32895
|
};
|
|
32241
32896
|
});
|
|
32242
32897
|
const nextUpdatedAt = (now, previous) => Math.max(DateTime.toEpochMillis(now), previous + 1);
|
|
32898
|
+
const decodeIndexScalar = Schema.decodeUnknownSync(IndexScalarValue);
|
|
32899
|
+
const indexDescriptors = (table, fields) => fields.map((field) => {
|
|
32900
|
+
const schema = table.fields[field];
|
|
32901
|
+
const descriptor = schema === void 0 ? void 0 : getValueDescriptor(schema);
|
|
32902
|
+
if (descriptor === void 0) throw new Error(`Index field '${field}' has no descriptor.`);
|
|
32903
|
+
return descriptor;
|
|
32904
|
+
});
|
|
32905
|
+
const storedIndexKey = (index, row) => {
|
|
32906
|
+
const fields = Schema.decodeSync(Schema.fromJsonString(Schema.JsonObject))(row.fields);
|
|
32907
|
+
const values = index.definition.fields.map((field) => decodeIndexScalar(fields[field]));
|
|
32908
|
+
return encodeIndexKey(index.descriptors, values, row.createdAt, row.id);
|
|
32909
|
+
};
|
|
32243
32910
|
var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-runtime/database/LocalDatabase") {
|
|
32244
32911
|
static layer = Layer.effect(LocalDatabase, Effect.gen(function* () {
|
|
32245
32912
|
const ids = yield* IdGenerator;
|
|
@@ -32283,6 +32950,31 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32283
32950
|
FROM documents
|
|
32284
32951
|
WHERE tableId = ${tableId}
|
|
32285
32952
|
ORDER BY createdAt ASC, id ASC
|
|
32953
|
+
`
|
|
32954
|
+
});
|
|
32955
|
+
const listIndexes = SqlSchema.findAll({
|
|
32956
|
+
Request: TableLookup,
|
|
32957
|
+
Result: StoredIndex,
|
|
32958
|
+
execute: ({ tableId }) => sql`
|
|
32959
|
+
SELECT id, identity, name, fields
|
|
32960
|
+
FROM indexes
|
|
32961
|
+
WHERE tableId = ${tableId}
|
|
32962
|
+
ORDER BY id ASC
|
|
32963
|
+
`
|
|
32964
|
+
});
|
|
32965
|
+
const insertIndex = SqlSchema.findOne({
|
|
32966
|
+
Request: Schema.Struct({
|
|
32967
|
+
tableId: TableId,
|
|
32968
|
+
identity: Schema.String,
|
|
32969
|
+
name: Schema.String,
|
|
32970
|
+
fields: Schema.String
|
|
32971
|
+
}),
|
|
32972
|
+
Result: StoredIndex,
|
|
32973
|
+
execute: ({ tableId, identity, name, fields }) => sql`
|
|
32974
|
+
INSERT INTO indexes (tableId, identity, name, fields)
|
|
32975
|
+
VALUES (${tableId}, ${identity}, ${name}, ${fields})
|
|
32976
|
+
ON CONFLICT (identity) DO UPDATE SET identity = excluded.identity
|
|
32977
|
+
RETURNING id, identity, name, fields
|
|
32286
32978
|
`
|
|
32287
32979
|
});
|
|
32288
32980
|
const currentRevision = SqlSchema.findOne({
|
|
@@ -32316,6 +33008,64 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32316
33008
|
definition
|
|
32317
33009
|
};
|
|
32318
33010
|
});
|
|
33011
|
+
const resolvedIndexes = Effect.fn("LocalDatabase.resolvedIndexes")(function* (table) {
|
|
33012
|
+
const stored = yield* listIndexes({ tableId: table.id });
|
|
33013
|
+
return Object.values(table.definition.indexes).map((definition) => {
|
|
33014
|
+
const identity = indexIdentity(table.id, definition.name, definition.fields);
|
|
33015
|
+
const row = stored.find((candidate) => candidate.identity === identity);
|
|
33016
|
+
if (row === void 0) throw new Error(`Index '${definition.name}' is not prepared.`);
|
|
33017
|
+
return {
|
|
33018
|
+
...row,
|
|
33019
|
+
definition,
|
|
33020
|
+
descriptors: indexDescriptors(table.definition, definition.fields)
|
|
33021
|
+
};
|
|
33022
|
+
});
|
|
33023
|
+
});
|
|
33024
|
+
const prepareIndexes = Effect.fn("LocalDatabase.prepareIndexes")(function* (schema) {
|
|
33025
|
+
for (const [tableName, definition] of Object.entries(schema[SchemaDefinitionTypeId])) {
|
|
33026
|
+
const table = yield* resolveTable(schema, tableName);
|
|
33027
|
+
const stored = yield* listIndexes({ tableId: table.id });
|
|
33028
|
+
const expected = /* @__PURE__ */ new Set();
|
|
33029
|
+
for (const index of Object.values(definition.indexes)) {
|
|
33030
|
+
const identity = indexIdentity(table.id, index.name, index.fields);
|
|
33031
|
+
expected.add(identity);
|
|
33032
|
+
if (stored.some((candidate) => candidate.identity === identity)) continue;
|
|
33033
|
+
const fields = JSON.stringify(index.fields);
|
|
33034
|
+
const resolved = {
|
|
33035
|
+
...yield* insertIndex({
|
|
33036
|
+
tableId: table.id,
|
|
33037
|
+
identity,
|
|
33038
|
+
name: index.name,
|
|
33039
|
+
fields
|
|
33040
|
+
}).pipe(Effect.catchTags({ NoSuchElementError: Effect.die })),
|
|
33041
|
+
definition: index,
|
|
33042
|
+
descriptors: indexDescriptors(definition, index.fields)
|
|
33043
|
+
};
|
|
33044
|
+
const documents = yield* collectStored({ tableId: table.id });
|
|
33045
|
+
yield* Effect.forEach(documents, (document) => sql`
|
|
33046
|
+
INSERT INTO indexEntries (indexId, tableId, documentId, key)
|
|
33047
|
+
VALUES (${resolved.id}, ${table.id}, ${document.id}, ${storedIndexKey(resolved, document)})
|
|
33048
|
+
`);
|
|
33049
|
+
}
|
|
33050
|
+
yield* Effect.forEach(stored.filter((index) => !expected.has(index.identity)), (index) => sql`DELETE FROM indexes WHERE id = ${index.id}`);
|
|
33051
|
+
}
|
|
33052
|
+
});
|
|
33053
|
+
const indexKeys = Effect.fn("LocalDatabase.indexKeys")(function* (table, row) {
|
|
33054
|
+
return (yield* resolvedIndexes(table)).map((index) => ({
|
|
33055
|
+
index,
|
|
33056
|
+
key: storedIndexKey(index, row),
|
|
33057
|
+
identity: indexIdentity(table.id, index.name, index.definition.fields)
|
|
33058
|
+
}));
|
|
33059
|
+
});
|
|
33060
|
+
const writeIndexEntries = Effect.fn("LocalDatabase.writeIndexEntries")(function* (table, row) {
|
|
33061
|
+
const keys = yield* indexKeys(table, row);
|
|
33062
|
+
yield* Effect.forEach(keys, ({ index, key }) => sql`
|
|
33063
|
+
INSERT INTO indexEntries (indexId, tableId, documentId, key)
|
|
33064
|
+
VALUES (${index.id}, ${table.id}, ${row.id}, ${key})
|
|
33065
|
+
ON CONFLICT (indexId, documentId) DO UPDATE SET key = excluded.key
|
|
33066
|
+
`);
|
|
33067
|
+
return keys;
|
|
33068
|
+
});
|
|
32319
33069
|
const find = Effect.fn("LocalDatabase.find")(function* (schema, tableName, id, dependencies) {
|
|
32320
33070
|
const table = yield* resolveTable(schema, tableName);
|
|
32321
33071
|
const documentId = GeneratedId.make(id);
|
|
@@ -32327,11 +33077,100 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32327
33077
|
if (Option.isNone(row)) return void 0;
|
|
32328
33078
|
return yield* toRuntimeDocument(table.definition, tableName, row.value, "get");
|
|
32329
33079
|
});
|
|
32330
|
-
const
|
|
33080
|
+
const queryDocuments = Effect.fn("LocalDatabase.queryDocuments")(function* (schema, tableName, selectedIndex, bounds, order, limit, pagination, dependencies) {
|
|
32331
33081
|
const table = yield* resolveTable(schema, tableName);
|
|
32332
|
-
|
|
32333
|
-
|
|
32334
|
-
|
|
33082
|
+
if (selectedIndex === void 0) {
|
|
33083
|
+
dependencies?.record(tableDependency(table.id));
|
|
33084
|
+
const queryIdentity = paginationQueryIdentity(table.id, null, void 0, void 0, order);
|
|
33085
|
+
const decodedCursor = pagination?.cursor === null || pagination === void 0 ? void 0 : decodePaginationCursor(pagination.cursor);
|
|
33086
|
+
if (decodedCursor !== void 0 && decodedCursor.query !== queryIdentity) throw new Error("A pagination cursor belongs to another query.");
|
|
33087
|
+
const cursor = decodedCursor?.position;
|
|
33088
|
+
if (cursor !== void 0 && cursor.type !== "Table") throw new Error("A pagination cursor has the wrong position type.");
|
|
33089
|
+
const clauses = ["tableId = ?"];
|
|
33090
|
+
const parameters = [table.id];
|
|
33091
|
+
if (cursor !== void 0) {
|
|
33092
|
+
const operator = order === "Asc" ? ">" : "<";
|
|
33093
|
+
clauses.push(`(createdAt ${operator} ? OR (createdAt = ? AND id ${operator} ?))`);
|
|
33094
|
+
parameters.push(cursor.createdAt, cursor.createdAt, cursor.id);
|
|
33095
|
+
}
|
|
33096
|
+
const storageLimit = pagination === void 0 ? limit : pagination.pageSize + 1;
|
|
33097
|
+
if (storageLimit !== void 0) parameters.push(storageLimit);
|
|
33098
|
+
const rows = yield* sql.unsafe(`SELECT id, createdAt, updatedAt, fields
|
|
33099
|
+
FROM documents
|
|
33100
|
+
WHERE ${clauses.join(" AND ")}
|
|
33101
|
+
ORDER BY createdAt ${order === "Asc" ? "ASC" : "DESC"}, id ${order === "Asc" ? "ASC" : "DESC"}${storageLimit === void 0 ? "" : " LIMIT ?"}`, parameters);
|
|
33102
|
+
const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(StoredDocument))(rows);
|
|
33103
|
+
const hasMore = pagination !== void 0 && decoded.length > pagination.pageSize;
|
|
33104
|
+
const pageRows = pagination === void 0 ? decoded : decoded.slice(0, pagination.pageSize);
|
|
33105
|
+
const documents = yield* Effect.forEach(pageRows, (row) => toRuntimeDocument(table.definition, tableName, row, "collect"));
|
|
33106
|
+
const last = pageRows.at(-1);
|
|
33107
|
+
return {
|
|
33108
|
+
documents,
|
|
33109
|
+
nextCursor: !hasMore || last === void 0 ? null : encodePaginationCursor(queryIdentity, {
|
|
33110
|
+
type: "Table",
|
|
33111
|
+
createdAt: last.createdAt,
|
|
33112
|
+
id: last.id
|
|
33113
|
+
})
|
|
33114
|
+
};
|
|
33115
|
+
}
|
|
33116
|
+
const index = (yield* resolvedIndexes(table)).find((candidate) => candidate.name === selectedIndex);
|
|
33117
|
+
if (index === void 0) throw new Error(`Unknown index '${selectedIndex}'.`);
|
|
33118
|
+
const identity = indexIdentity(table.id, index.name, index.definition.fields);
|
|
33119
|
+
const queryIdentity = paginationQueryIdentity(table.id, identity, bounds.lower, bounds.upper, order);
|
|
33120
|
+
const decodedCursor = pagination?.cursor === null || pagination === void 0 ? void 0 : decodePaginationCursor(pagination.cursor);
|
|
33121
|
+
if (decodedCursor !== void 0 && decodedCursor.query !== queryIdentity) throw new Error("A pagination cursor belongs to another query.");
|
|
33122
|
+
const cursor = decodedCursor?.position;
|
|
33123
|
+
if (cursor !== void 0 && cursor.type !== "Index") throw new Error("A pagination cursor has the wrong position type.");
|
|
33124
|
+
const cursorLower = cursor !== void 0 && order === "Asc" ? successorIndexPrefix(cursor.key) : void 0;
|
|
33125
|
+
const cursorUpper = cursor !== void 0 && order === "Desc" ? cursor.key : void 0;
|
|
33126
|
+
const effectiveBounds = {
|
|
33127
|
+
lower: bounds.lower === void 0 ? cursorLower : cursorLower === void 0 || bounds.lower > cursorLower ? bounds.lower : cursorLower,
|
|
33128
|
+
upper: bounds.upper === void 0 ? cursorUpper : cursorUpper === void 0 || bounds.upper < cursorUpper ? bounds.upper : cursorUpper
|
|
33129
|
+
};
|
|
33130
|
+
const clauses = ["entries.indexId = ?"];
|
|
33131
|
+
const parameters = [index.id];
|
|
33132
|
+
if (effectiveBounds.lower !== void 0) {
|
|
33133
|
+
clauses.push("entries.key >= ?");
|
|
33134
|
+
parameters.push(effectiveBounds.lower);
|
|
33135
|
+
}
|
|
33136
|
+
if (effectiveBounds.upper !== void 0) {
|
|
33137
|
+
clauses.push("entries.key < ?");
|
|
33138
|
+
parameters.push(effectiveBounds.upper);
|
|
33139
|
+
}
|
|
33140
|
+
const storageLimit = pagination === void 0 ? limit : pagination.pageSize + 1;
|
|
33141
|
+
if (storageLimit !== void 0) parameters.push(storageLimit);
|
|
33142
|
+
const rows = yield* sql.unsafe(`SELECT documents.id, documents.createdAt, documents.updatedAt, documents.fields,
|
|
33143
|
+
entries.key
|
|
33144
|
+
FROM indexEntries AS entries
|
|
33145
|
+
JOIN documents
|
|
33146
|
+
ON documents.tableId = entries.tableId
|
|
33147
|
+
AND documents.id = entries.documentId
|
|
33148
|
+
WHERE ${clauses.join(" AND ")}
|
|
33149
|
+
ORDER BY entries.key ${order === "Asc" ? "ASC" : "DESC"}${storageLimit === void 0 ? "" : " LIMIT ?"}`, parameters);
|
|
33150
|
+
const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(IndexedStoredDocument))(rows);
|
|
33151
|
+
const hasMore = pagination !== void 0 && decoded.length > pagination.pageSize;
|
|
33152
|
+
const pageRows = pagination === void 0 ? decoded : decoded.slice(0, pagination.pageSize);
|
|
33153
|
+
let dependencyBounds = effectiveBounds;
|
|
33154
|
+
if (pagination === void 0 ? limit !== void 0 && limit > 0 && decoded.length === limit : hasMore) {
|
|
33155
|
+
const last = pageRows.at(-1);
|
|
33156
|
+
if (last !== void 0) dependencyBounds = order === "Asc" ? {
|
|
33157
|
+
...effectiveBounds,
|
|
33158
|
+
upper: successorIndexPrefix(EncodedIndexKey.make(last.key))
|
|
33159
|
+
} : {
|
|
33160
|
+
...effectiveBounds,
|
|
33161
|
+
lower: EncodedIndexKey.make(last.key)
|
|
33162
|
+
};
|
|
33163
|
+
}
|
|
33164
|
+
dependencies?.record(indexRangeDependency(table.id, identity, dependencyBounds.lower, dependencyBounds.upper));
|
|
33165
|
+
const documents = yield* Effect.forEach(pageRows, (row) => toRuntimeDocument(table.definition, tableName, row, "collect"));
|
|
33166
|
+
const last = pageRows.at(-1);
|
|
33167
|
+
return {
|
|
33168
|
+
documents,
|
|
33169
|
+
nextCursor: !hasMore || last === void 0 ? null : encodePaginationCursor(queryIdentity, {
|
|
33170
|
+
type: "Index",
|
|
33171
|
+
key: EncodedIndexKey.make(last.key)
|
|
33172
|
+
})
|
|
33173
|
+
};
|
|
32335
33174
|
});
|
|
32336
33175
|
const insert = Effect.fn("LocalDatabase.insert")(function* (schema, tableName, value, invalidations) {
|
|
32337
33176
|
const table = yield* resolveTable(schema, tableName);
|
|
@@ -32342,71 +33181,196 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32342
33181
|
INSERT INTO documents (id, tableId, createdAt, updatedAt, fields)
|
|
32343
33182
|
VALUES (${id}, ${table.id}, ${now}, ${now}, ${fields})
|
|
32344
33183
|
`;
|
|
32345
|
-
|
|
33184
|
+
const keys = yield* writeIndexEntries(table, {
|
|
33185
|
+
id,
|
|
33186
|
+
createdAt: now,
|
|
33187
|
+
updatedAt: now,
|
|
33188
|
+
fields
|
|
33189
|
+
});
|
|
33190
|
+
invalidations.record(tableDependency(table.id), documentDependency(table.id, id), ...keys.map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
|
|
32346
33191
|
return id;
|
|
32347
33192
|
});
|
|
32348
33193
|
const deleteDocument = Effect.fn("LocalDatabase.delete")(function* (schema, tableName, id, invalidations) {
|
|
32349
|
-
const table = yield* resolveTable(schema, tableName);
|
|
33194
|
+
const table = yield* resolveTable(schema, tableName).pipe(Effect.orDie);
|
|
32350
33195
|
const documentId = GeneratedId.make(id);
|
|
32351
|
-
|
|
32352
|
-
|
|
33196
|
+
const stored = yield* findStored({
|
|
33197
|
+
id,
|
|
33198
|
+
tableId: table.id
|
|
33199
|
+
}).pipe(Effect.orDie);
|
|
33200
|
+
if (Option.isNone(stored)) return yield* Effect.fail(documentNotFound(tableName, id));
|
|
33201
|
+
const keys = yield* indexKeys(table, stored.value).pipe(Effect.orDie);
|
|
33202
|
+
yield* sql`DELETE FROM documents WHERE id = ${id} AND tableId = ${table.id}`.pipe(Effect.orDie);
|
|
33203
|
+
invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId), ...keys.map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
|
|
32353
33204
|
});
|
|
32354
33205
|
const patch = Effect.fn("LocalDatabase.patch")(function* (schema, tableName, id, value, invalidations) {
|
|
32355
|
-
const table = yield* resolveTable(schema, tableName);
|
|
33206
|
+
const table = yield* resolveTable(schema, tableName).pipe(Effect.orDie);
|
|
32356
33207
|
const documentId = GeneratedId.make(id);
|
|
32357
33208
|
const stored = yield* findStored({
|
|
32358
33209
|
id,
|
|
32359
33210
|
tableId: table.id
|
|
32360
|
-
});
|
|
32361
|
-
if (Option.
|
|
32362
|
-
|
|
32363
|
-
|
|
32364
|
-
|
|
32365
|
-
|
|
32366
|
-
|
|
32367
|
-
|
|
32368
|
-
|
|
32369
|
-
|
|
32370
|
-
|
|
32371
|
-
|
|
32372
|
-
|
|
32373
|
-
|
|
32374
|
-
|
|
33211
|
+
}).pipe(Effect.orDie);
|
|
33212
|
+
if (Option.isNone(stored)) return yield* Effect.fail(documentNotFound(tableName, id));
|
|
33213
|
+
const oldKeys = yield* indexKeys(table, stored.value).pipe(Effect.orDie);
|
|
33214
|
+
const current = yield* decodeFields(table.definition, tableName, stored.value, "patch").pipe(Effect.orDie);
|
|
33215
|
+
const fields = yield* encodeFields(table.definition, tableName, {
|
|
33216
|
+
...current,
|
|
33217
|
+
...value
|
|
33218
|
+
}, "patch", id).pipe(Effect.orDie);
|
|
33219
|
+
const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
|
|
33220
|
+
yield* sql`
|
|
33221
|
+
UPDATE documents
|
|
33222
|
+
SET fields = ${fields}, updatedAt = ${updatedAt}
|
|
33223
|
+
WHERE id = ${id} AND tableId = ${table.id}
|
|
33224
|
+
`.pipe(Effect.orDie);
|
|
33225
|
+
const newKeys = yield* writeIndexEntries(table, {
|
|
33226
|
+
...stored.value,
|
|
33227
|
+
fields,
|
|
33228
|
+
updatedAt
|
|
33229
|
+
}).pipe(Effect.orDie);
|
|
33230
|
+
invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId), ...[...oldKeys, ...newKeys].map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
|
|
32375
33231
|
});
|
|
32376
33232
|
const replace = Effect.fn("LocalDatabase.replace")(function* (schema, tableName, id, value, invalidations) {
|
|
32377
|
-
const table = yield* resolveTable(schema, tableName);
|
|
33233
|
+
const table = yield* resolveTable(schema, tableName).pipe(Effect.orDie);
|
|
32378
33234
|
const documentId = GeneratedId.make(id);
|
|
32379
33235
|
const stored = yield* findStored({
|
|
32380
33236
|
id,
|
|
32381
33237
|
tableId: table.id
|
|
32382
|
-
});
|
|
32383
|
-
if (Option.
|
|
32384
|
-
|
|
32385
|
-
|
|
32386
|
-
|
|
32387
|
-
|
|
32388
|
-
|
|
32389
|
-
|
|
32390
|
-
|
|
32391
|
-
|
|
32392
|
-
|
|
32393
|
-
|
|
32394
|
-
|
|
32395
|
-
|
|
32396
|
-
|
|
32397
|
-
|
|
33238
|
+
}).pipe(Effect.orDie);
|
|
33239
|
+
if (Option.isNone(stored)) return yield* Effect.fail(documentNotFound(tableName, id));
|
|
33240
|
+
const oldKeys = yield* indexKeys(table, stored.value).pipe(Effect.orDie);
|
|
33241
|
+
const fields = yield* encodeFields(table.definition, tableName, value, "replace", id).pipe(Effect.orDie);
|
|
33242
|
+
const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
|
|
33243
|
+
yield* sql`
|
|
33244
|
+
UPDATE documents
|
|
33245
|
+
SET fields = ${fields}, updatedAt = ${updatedAt}
|
|
33246
|
+
WHERE id = ${id} AND tableId = ${table.id}
|
|
33247
|
+
`.pipe(Effect.orDie);
|
|
33248
|
+
const newKeys = yield* writeIndexEntries(table, {
|
|
33249
|
+
...stored.value,
|
|
33250
|
+
fields,
|
|
33251
|
+
updatedAt
|
|
33252
|
+
}).pipe(Effect.orDie);
|
|
33253
|
+
invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId), ...[...oldKeys, ...newKeys].map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
|
|
32398
33254
|
});
|
|
33255
|
+
const makeReader = (schema, dependencies) => {
|
|
33256
|
+
const databaseQuery = (tableName, selectedIndex, selectedBounds = {}, order = "Asc") => {
|
|
33257
|
+
const table = schema[SchemaDefinitionTypeId][tableName];
|
|
33258
|
+
if (table === void 0) throw new Error(`Unknown database table '${tableName}'.`);
|
|
33259
|
+
const run = (limit) => queryDocuments(schema, tableName, selectedIndex, selectedBounds, order, limit, void 0, dependencies).pipe(Effect.map(({ documents }) => documents), Effect.orDie);
|
|
33260
|
+
const runPage = (pagination) => queryDocuments(schema, tableName, selectedIndex, selectedBounds, order, void 0, pagination, dependencies).pipe(Effect.orDie);
|
|
33261
|
+
const validateLimit = (count) => {
|
|
33262
|
+
if (!Number.isInteger(count) || count < 0 || count > 1e3) throw new Error("Query limits must be integers between 0 and 1000.");
|
|
33263
|
+
return count;
|
|
33264
|
+
};
|
|
33265
|
+
return Object.freeze({
|
|
33266
|
+
index: (name, buildRange) => {
|
|
33267
|
+
if (selectedIndex !== void 0) throw new Error("A query can select only one index.");
|
|
33268
|
+
const definition = table.indexes[name];
|
|
33269
|
+
if (definition === void 0) throw new Error(`Unknown index '${name}' on table '${tableName}'.`);
|
|
33270
|
+
if (buildRange === void 0) return databaseQuery(tableName, name, {}, order);
|
|
33271
|
+
const fields = [
|
|
33272
|
+
...definition.fields,
|
|
33273
|
+
"createdAt",
|
|
33274
|
+
"id"
|
|
33275
|
+
];
|
|
33276
|
+
const descriptors = [
|
|
33277
|
+
...indexDescriptors(table, definition.fields),
|
|
33278
|
+
{ type: "date" },
|
|
33279
|
+
{
|
|
33280
|
+
type: "id",
|
|
33281
|
+
table: tableName
|
|
33282
|
+
}
|
|
33283
|
+
];
|
|
33284
|
+
const equal = [];
|
|
33285
|
+
let lower;
|
|
33286
|
+
let upper;
|
|
33287
|
+
let position = 0;
|
|
33288
|
+
const encodeValue = (field, value) => {
|
|
33289
|
+
const codec = field === "createdAt" ? Schema.DateFromMillis : field === "id" ? GeneratedId : table.fields[field];
|
|
33290
|
+
if (codec === void 0) throw new Error(`Unknown index field '${field}'.`);
|
|
33291
|
+
return decodeIndexScalar(Schema.encodeUnknownSync(codec)(value));
|
|
33292
|
+
};
|
|
33293
|
+
const expectField = (field) => {
|
|
33294
|
+
const expected = fields[position];
|
|
33295
|
+
if (field !== expected) throw new Error(`Expected index field '${expected}', received '${field}'.`);
|
|
33296
|
+
};
|
|
33297
|
+
const builder = {
|
|
33298
|
+
eq: (field, value) => {
|
|
33299
|
+
if (lower !== void 0 || upper !== void 0) throw new Error("Equality cannot follow an index bound.");
|
|
33300
|
+
expectField(field);
|
|
33301
|
+
equal.push(encodeValue(field, value));
|
|
33302
|
+
position += 1;
|
|
33303
|
+
return builder;
|
|
33304
|
+
},
|
|
33305
|
+
gt: (field, value) => {
|
|
33306
|
+
expectField(field);
|
|
33307
|
+
if (lower !== void 0) throw new Error("An index range has one lower bound.");
|
|
33308
|
+
lower = {
|
|
33309
|
+
value: encodeValue(field, value),
|
|
33310
|
+
inclusive: false
|
|
33311
|
+
};
|
|
33312
|
+
return builder;
|
|
33313
|
+
},
|
|
33314
|
+
gte: (field, value) => {
|
|
33315
|
+
expectField(field);
|
|
33316
|
+
if (lower !== void 0) throw new Error("An index range has one lower bound.");
|
|
33317
|
+
lower = {
|
|
33318
|
+
value: encodeValue(field, value),
|
|
33319
|
+
inclusive: true
|
|
33320
|
+
};
|
|
33321
|
+
return builder;
|
|
33322
|
+
},
|
|
33323
|
+
lt: (field, value) => {
|
|
33324
|
+
expectField(field);
|
|
33325
|
+
if (upper !== void 0) throw new Error("An index range has one upper bound.");
|
|
33326
|
+
upper = {
|
|
33327
|
+
value: encodeValue(field, value),
|
|
33328
|
+
inclusive: false
|
|
33329
|
+
};
|
|
33330
|
+
return builder;
|
|
33331
|
+
},
|
|
33332
|
+
lte: (field, value) => {
|
|
33333
|
+
expectField(field);
|
|
33334
|
+
if (upper !== void 0) throw new Error("An index range has one upper bound.");
|
|
33335
|
+
upper = {
|
|
33336
|
+
value: encodeValue(field, value),
|
|
33337
|
+
inclusive: true
|
|
33338
|
+
};
|
|
33339
|
+
return builder;
|
|
33340
|
+
}
|
|
33341
|
+
};
|
|
33342
|
+
buildRange(builder);
|
|
33343
|
+
return databaseQuery(tableName, name, encodeIndexRange(descriptors, equal, lower, upper), order);
|
|
33344
|
+
},
|
|
33345
|
+
order: (direction) => databaseQuery(tableName, selectedIndex, selectedBounds, direction === "asc" ? "Asc" : "Desc"),
|
|
33346
|
+
collect: () => resultFromEffect(run()),
|
|
33347
|
+
take: (count) => resultFromEffect(run(validateLimit(count))),
|
|
33348
|
+
first: () => resultFromEffect(run(1).pipe(Effect.map((documents) => documents[0]))),
|
|
33349
|
+
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])))),
|
|
33350
|
+
paginate: (options) => resultFromEffect(runPage(options).pipe(Effect.map(({ documents, nextCursor }) => ({
|
|
33351
|
+
items: documents,
|
|
33352
|
+
nextCursor
|
|
33353
|
+
}))))
|
|
33354
|
+
});
|
|
33355
|
+
};
|
|
33356
|
+
return Object.freeze({
|
|
33357
|
+
find: (tableName, id) => resultFromEffect(find(schema, tableName, id, dependencies).pipe(Effect.orDie)),
|
|
33358
|
+
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)))),
|
|
33359
|
+
query: (tableName) => databaseQuery(tableName)
|
|
33360
|
+
});
|
|
33361
|
+
};
|
|
32399
33362
|
const makeWriter = (schema, invalidations) => {
|
|
32400
33363
|
const reader = makeReader(schema);
|
|
32401
33364
|
return Object.freeze({
|
|
32402
33365
|
...reader,
|
|
32403
|
-
delete: (tableName, id) => resultFromEffect(deleteDocument(schema, tableName, id, invalidations)
|
|
33366
|
+
delete: (tableName, id) => resultFromEffect(deleteDocument(schema, tableName, id, invalidations)),
|
|
32404
33367
|
insert: (tableName, value) => resultFromEffect(insert(schema, tableName, value, invalidations).pipe(Effect.orDie)),
|
|
32405
|
-
patch: (tableName, id, value) => resultFromEffect(patch(schema, tableName, id, value, invalidations)
|
|
32406
|
-
replace: (tableName, id, value) => resultFromEffect(replace(schema, tableName, id, value, invalidations)
|
|
33368
|
+
patch: (tableName, id, value) => resultFromEffect(patch(schema, tableName, id, value, invalidations)),
|
|
33369
|
+
replace: (tableName, id, value) => resultFromEffect(replace(schema, tableName, id, value, invalidations))
|
|
32407
33370
|
});
|
|
32408
33371
|
};
|
|
32409
33372
|
const trackedQueryTransaction = (schema, use) => sql.withTransaction(Effect.gen(function* () {
|
|
33373
|
+
yield* prepareIndexes(schema).pipe(Effect.orDie);
|
|
32410
33374
|
const dependencies = makeDependencyRecorder();
|
|
32411
33375
|
const value = yield* use(Object.freeze({ db: makeReader(schema, dependencies) }));
|
|
32412
33376
|
const { revision: observedRevision } = yield* currentRevision(void 0).pipe(Effect.orDie);
|
|
@@ -32417,6 +33381,7 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32417
33381
|
};
|
|
32418
33382
|
}));
|
|
32419
33383
|
const trackedMutationTransaction = (schema, use) => sql.withTransaction(Effect.gen(function* () {
|
|
33384
|
+
yield* prepareIndexes(schema).pipe(Effect.orDie);
|
|
32420
33385
|
const invalidations = makeDependencyRecorder();
|
|
32421
33386
|
const value = yield* use(Object.freeze({ db: makeWriter(schema, invalidations) }));
|
|
32422
33387
|
const { revision: committedRevision } = yield* advanceRevision(void 0).pipe(Effect.orDie);
|
|
@@ -32512,12 +33477,19 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
|
|
|
32512
33477
|
onFailure: (error) => encodeFailure(resolved.definition, error),
|
|
32513
33478
|
onSuccess: (value) => encodeSuccess(resolved.definition, value)
|
|
32514
33479
|
}));
|
|
32515
|
-
return yield* (kind === "Query" ? database.trackedQueryTransaction(resolved.schema, invoke).pipe(Effect.map(({ dependencies, observedRevision, value }) =>
|
|
32516
|
-
type
|
|
32517
|
-
result
|
|
32518
|
-
|
|
32519
|
-
|
|
32520
|
-
|
|
33480
|
+
return yield* (kind === "Query" ? database.trackedQueryTransaction(resolved.schema, invoke).pipe(Effect.map(({ dependencies, observedRevision, value }) => {
|
|
33481
|
+
const files = value.type === "Success" && value.value !== void 0 ? encodedFileOccurrencesOf(value.value) : [];
|
|
33482
|
+
const result = {
|
|
33483
|
+
type: "Query",
|
|
33484
|
+
result: value,
|
|
33485
|
+
dependencies,
|
|
33486
|
+
observedRevision
|
|
33487
|
+
};
|
|
33488
|
+
return files.length === 0 ? result : {
|
|
33489
|
+
...result,
|
|
33490
|
+
files
|
|
33491
|
+
};
|
|
33492
|
+
})) : mutationSemaphore.withPermits(1)(database.trackedMutationTransaction(resolved.schema, (context) => invoke(context).pipe(Effect.flatMap((result) => result.type === "Failure" ? Effect.fail(MutationApplicationFailure.make({ error: result.error })) : Effect.succeed(result)))).pipe(Effect.map(({ committedRevision, invalidations, value }) => ({
|
|
32521
33493
|
type: "Mutation",
|
|
32522
33494
|
result: value,
|
|
32523
33495
|
invalidations,
|
|
@@ -32566,6 +33538,252 @@ const functionRuntimeLayer = Layer.effect(FunctionRuntime, Effect.gen(function*
|
|
|
32566
33538
|
}) });
|
|
32567
33539
|
}));
|
|
32568
33540
|
//#endregion
|
|
33541
|
+
//#region ../contracts/dist/schema/file-content.js
|
|
33542
|
+
const ascii = (bytes) => {
|
|
33543
|
+
let value = "";
|
|
33544
|
+
for (let offset = 0; offset < bytes.length; offset += 16384) value += String.fromCharCode(...bytes.subarray(offset, offset + 16384));
|
|
33545
|
+
return value;
|
|
33546
|
+
};
|
|
33547
|
+
const startsWith = (bytes, signature) => signature.every((byte, index) => bytes[index] === byte);
|
|
33548
|
+
const endsWith = (bytes, signature) => signature.every((byte, index) => bytes[bytes.length - signature.length + index] === byte);
|
|
33549
|
+
const validJpeg = (bytes) => bytes.length >= 4 && startsWith(bytes, [
|
|
33550
|
+
255,
|
|
33551
|
+
216,
|
|
33552
|
+
255
|
|
33553
|
+
]) && endsWith(bytes, [255, 217]);
|
|
33554
|
+
const validPng = (bytes) => bytes.length >= 20 && startsWith(bytes, [
|
|
33555
|
+
137,
|
|
33556
|
+
80,
|
|
33557
|
+
78,
|
|
33558
|
+
71,
|
|
33559
|
+
13,
|
|
33560
|
+
10,
|
|
33561
|
+
26,
|
|
33562
|
+
10
|
|
33563
|
+
]) && ascii(bytes.subarray(bytes.length - 12, bytes.length - 8)) === "IEND";
|
|
33564
|
+
const validGif = (bytes) => bytes.length >= 14 && (ascii(bytes.subarray(0, 6)) === "GIF87a" || ascii(bytes.subarray(0, 6)) === "GIF89a") && bytes.at(-1) === 59;
|
|
33565
|
+
const validWebp = (bytes) => {
|
|
33566
|
+
if (bytes.length < 12 || ascii(bytes.subarray(0, 4)) !== "RIFF" || ascii(bytes.subarray(8, 12)) !== "WEBP") return false;
|
|
33567
|
+
return ((bytes[4] ?? 0) | (bytes[5] ?? 0) << 8 | (bytes[6] ?? 0) << 16 | (bytes[7] ?? 0) << 24) + 8 === bytes.length;
|
|
33568
|
+
};
|
|
33569
|
+
const validAvif = (bytes) => {
|
|
33570
|
+
if (bytes.length < 16 || ascii(bytes.subarray(4, 8)) !== "ftyp") return false;
|
|
33571
|
+
const boxLength = (bytes[0] ?? 0) << 24 | (bytes[1] ?? 0) << 16 | (bytes[2] ?? 0) << 8 | (bytes[3] ?? 0);
|
|
33572
|
+
if (boxLength < 16 || boxLength > bytes.length) return false;
|
|
33573
|
+
for (let offset = 8; offset + 4 <= boxLength; offset += 4) {
|
|
33574
|
+
const brand = ascii(bytes.subarray(offset, offset + 4));
|
|
33575
|
+
if (brand === "avif" || brand === "avis") return true;
|
|
33576
|
+
}
|
|
33577
|
+
return false;
|
|
33578
|
+
};
|
|
33579
|
+
const validateApplicationFile = (bytes, format, declaredSize) => {
|
|
33580
|
+
if (bytes.byteLength !== declaredSize) return {
|
|
33581
|
+
valid: false,
|
|
33582
|
+
message: "The uploaded byte length does not match its preparation."
|
|
33583
|
+
};
|
|
33584
|
+
if (bytes.byteLength === 0 || bytes.byteLength > fileLimits.fileBytes) return {
|
|
33585
|
+
valid: false,
|
|
33586
|
+
message: `Application files must be between 1 and ${fileLimits.fileBytes} bytes.`
|
|
33587
|
+
};
|
|
33588
|
+
return (format === "jpeg" ? validJpeg(bytes) : format === "png" ? validPng(bytes) : format === "gif" ? validGif(bytes) : format === "webp" ? validWebp(bytes) : validAvif(bytes)) ? { valid: true } : {
|
|
33589
|
+
valid: false,
|
|
33590
|
+
message: `The upload is not a valid ${format} image.`
|
|
33591
|
+
};
|
|
33592
|
+
};
|
|
33593
|
+
//#endregion
|
|
33594
|
+
//#region src/dev-runtime/files.ts
|
|
33595
|
+
Schema.Struct({
|
|
33596
|
+
fileId: FileId,
|
|
33597
|
+
name: Schema.String,
|
|
33598
|
+
format: RuntimeFileOccurrence.fields.file.fields.format,
|
|
33599
|
+
size: Schema.Natural,
|
|
33600
|
+
ready: Schema.Literals([0, 1])
|
|
33601
|
+
});
|
|
33602
|
+
Schema.Struct({ fields: Schema.String });
|
|
33603
|
+
const JsonObjectString = Schema.fromJsonString(Schema.JsonObject);
|
|
33604
|
+
const LocalApplicationFiles = Context.Reference("ignotum/dev-runtime/files/LocalApplicationFiles", { defaultValue: () => ({
|
|
33605
|
+
admit: (_invocationId, _functionAddress, args) => Effect.succeed(encodedFileOccurrencesOf(args)),
|
|
33606
|
+
grants: (_subscriptionId, files) => Effect.succeed(files.map((file) => ({
|
|
33607
|
+
...file,
|
|
33608
|
+
url: ""
|
|
33609
|
+
}))),
|
|
33610
|
+
prepare: (_invocationId, _functionAddress, _args, files) => Effect.succeed(files),
|
|
33611
|
+
upload: () => Effect.die("Local application file storage is unavailable."),
|
|
33612
|
+
releaseGrants: () => Effect.void,
|
|
33613
|
+
readGrant: () => Effect.succeed(Option.none()),
|
|
33614
|
+
reconcile: Effect.void,
|
|
33615
|
+
releasePreparation: () => Effect.void
|
|
33616
|
+
}) });
|
|
33617
|
+
const sameFile = (left, right) => left.id === right.id && left.format === right.format && left.name === right.name && left.size === right.size;
|
|
33618
|
+
const validFilename = (name) => {
|
|
33619
|
+
const bytes = new TextEncoder().encode(name).byteLength;
|
|
33620
|
+
const hasControlCharacter = Array.from(name).some((character) => {
|
|
33621
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
33622
|
+
return codePoint <= 31 || codePoint === 127;
|
|
33623
|
+
});
|
|
33624
|
+
return bytes > 0 && bytes <= fileLimits.filenameBytes && !hasControlCharacter && !name.includes("/") && !name.includes("\\");
|
|
33625
|
+
};
|
|
33626
|
+
const localApplicationFilesLayer = (appDirectory) => Layer.effect(LocalApplicationFiles, Effect.gen(function* () {
|
|
33627
|
+
const ids = yield* IdGenerator;
|
|
33628
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
33629
|
+
const path = yield* Path$1.Path;
|
|
33630
|
+
const sql = yield* SqlClient.SqlClient;
|
|
33631
|
+
const directory = path.join(appDirectory, ".ignotum", "files");
|
|
33632
|
+
yield* fileSystem.makeDirectory(directory, { recursive: true }).pipe(Effect.orDie);
|
|
33633
|
+
const preparations = /* @__PURE__ */ new Map();
|
|
33634
|
+
const uploadFiles = /* @__PURE__ */ new Map();
|
|
33635
|
+
const grants = /* @__PURE__ */ new Map();
|
|
33636
|
+
const objectPath = (fileId) => path.join(directory, fileId);
|
|
33637
|
+
const prepare = Effect.fn("LocalApplicationFiles.prepare")(function* (invocationId, functionAddress, args, files) {
|
|
33638
|
+
const existing = preparations.get(invocationId);
|
|
33639
|
+
if (existing !== void 0) {
|
|
33640
|
+
if (existing.function !== functionAddress || encodeCanonicalJson(existing.args) !== encodeCanonicalJson(args) || encodeCanonicalJson(existing.files) !== encodeCanonicalJson(files)) throw new Error("The invocation ID already belongs to another file preparation.");
|
|
33641
|
+
return files.map((occurrence) => {
|
|
33642
|
+
const token = existing.uploads.get(occurrence.file.id);
|
|
33643
|
+
return token === void 0 ? occurrence : {
|
|
33644
|
+
...occurrence,
|
|
33645
|
+
url: `${fileUploadUrlPrefix}${token}`
|
|
33646
|
+
};
|
|
33647
|
+
});
|
|
33648
|
+
}
|
|
33649
|
+
const all = new Map(encodedFileOccurrencesOf(args).map((occurrence) => [encodeCanonicalJson(occurrence.path), occurrence.file]));
|
|
33650
|
+
const distinct = /* @__PURE__ */ new Map();
|
|
33651
|
+
for (const occurrence of files) {
|
|
33652
|
+
const arg = all.get(encodeCanonicalJson(occurrence.path));
|
|
33653
|
+
if (arg === void 0 || !sameFile(arg, occurrence.file) || !validFilename(occurrence.file.name)) throw new Error("Prepared files do not match the mutation arguments.");
|
|
33654
|
+
const prior = distinct.get(occurrence.file.id);
|
|
33655
|
+
if (prior !== void 0 && !sameFile(prior, occurrence.file)) throw new Error("One file ID has conflicting metadata.");
|
|
33656
|
+
distinct.set(occurrence.file.id, occurrence.file);
|
|
33657
|
+
}
|
|
33658
|
+
if (distinct.size > fileLimits.filesPerMutation) throw new Error("Too many files.");
|
|
33659
|
+
if ([...distinct.values()].reduce((total, file) => total + file.size, 0) > fileLimits.mutationBytes) throw new Error("File uploads are too large.");
|
|
33660
|
+
const staged = yield* sql`
|
|
33661
|
+
SELECT COALESCE(SUM(size), 0) AS bytes FROM applicationFiles WHERE ready = 0
|
|
33662
|
+
`;
|
|
33663
|
+
let additionalStagedBytes = 0;
|
|
33664
|
+
for (const file of distinct.values()) {
|
|
33665
|
+
const stored = (yield* sql`
|
|
33666
|
+
SELECT fileId, name, format, size, ready FROM applicationFiles WHERE fileId = ${file.id}
|
|
33667
|
+
`)[0];
|
|
33668
|
+
if (stored === void 0) additionalStagedBytes += file.size;
|
|
33669
|
+
else if (!sameFile({
|
|
33670
|
+
id: stored.fileId,
|
|
33671
|
+
name: stored.name,
|
|
33672
|
+
format: stored.format,
|
|
33673
|
+
size: stored.size
|
|
33674
|
+
}, file)) throw new Error(`File '${file.id}' has conflicting metadata.`);
|
|
33675
|
+
}
|
|
33676
|
+
if ((staged[0]?.bytes ?? 0) + additionalStagedBytes > fileLimits.stagedBytesPerApp) throw new Error("The app has too many staged file bytes.");
|
|
33677
|
+
const uploads = /* @__PURE__ */ new Map();
|
|
33678
|
+
for (const file of distinct.values()) {
|
|
33679
|
+
const stored = (yield* sql`
|
|
33680
|
+
SELECT fileId, name, format, size, ready FROM applicationFiles WHERE fileId = ${file.id}
|
|
33681
|
+
`)[0];
|
|
33682
|
+
if (stored !== void 0 && stored.ready === 1) continue;
|
|
33683
|
+
const token = yield* ids.generate(FileUploadToken);
|
|
33684
|
+
uploads.set(file.id, token);
|
|
33685
|
+
uploadFiles.set(token, file);
|
|
33686
|
+
yield* sql`
|
|
33687
|
+
INSERT INTO applicationFiles (fileId, name, format, size, ready)
|
|
33688
|
+
VALUES (${file.id}, ${file.name}, ${file.format}, ${file.size}, 0)
|
|
33689
|
+
ON CONFLICT(fileId) DO UPDATE SET
|
|
33690
|
+
name = excluded.name, format = excluded.format, size = excluded.size
|
|
33691
|
+
`;
|
|
33692
|
+
}
|
|
33693
|
+
preparations.set(invocationId, {
|
|
33694
|
+
function: functionAddress,
|
|
33695
|
+
args,
|
|
33696
|
+
files,
|
|
33697
|
+
uploads
|
|
33698
|
+
});
|
|
33699
|
+
return files.map((occurrence) => {
|
|
33700
|
+
const token = uploads.get(occurrence.file.id);
|
|
33701
|
+
return token === void 0 ? occurrence : {
|
|
33702
|
+
...occurrence,
|
|
33703
|
+
url: `${fileUploadUrlPrefix}${token}`
|
|
33704
|
+
};
|
|
33705
|
+
});
|
|
33706
|
+
});
|
|
33707
|
+
const admit = Effect.fn("LocalApplicationFiles.admit")(function* (invocationId, functionAddress, args) {
|
|
33708
|
+
const preparation = preparations.get(invocationId);
|
|
33709
|
+
if (preparation !== void 0 && (preparation.function !== functionAddress || encodeCanonicalJson(preparation.args) !== encodeCanonicalJson(args))) throw new Error("The mutation does not match its file preparation.");
|
|
33710
|
+
for (const occurrence of preparation?.files ?? []) if ((yield* sql`
|
|
33711
|
+
SELECT ready FROM applicationFiles WHERE fileId = ${occurrence.file.id}
|
|
33712
|
+
`)[0]?.ready !== 1) throw new Error("A prepared file has not finished uploading.");
|
|
33713
|
+
const occurrences = encodedFileOccurrencesOf(args);
|
|
33714
|
+
for (const occurrence of occurrences) if ((yield* sql`
|
|
33715
|
+
SELECT ready FROM applicationFiles WHERE fileId = ${occurrence.file.id}
|
|
33716
|
+
`)[0]?.ready !== 1) throw new Error(`File '${occurrence.file.id}' is unavailable.`);
|
|
33717
|
+
return occurrences;
|
|
33718
|
+
});
|
|
33719
|
+
const upload = Effect.fn("LocalApplicationFiles.upload")(function* (token, bytes) {
|
|
33720
|
+
const file = uploadFiles.get(token);
|
|
33721
|
+
if (file === void 0) throw new Error("The local file upload ticket is invalid.");
|
|
33722
|
+
const validation = validateApplicationFile(bytes, file.format, file.size);
|
|
33723
|
+
if (!validation.valid) throw new Error(validation.message);
|
|
33724
|
+
yield* fileSystem.writeFile(objectPath(file.id), bytes).pipe(Effect.orDie);
|
|
33725
|
+
yield* sql`UPDATE applicationFiles SET ready = 1 WHERE fileId = ${file.id}`;
|
|
33726
|
+
uploadFiles.delete(token);
|
|
33727
|
+
return "Stored";
|
|
33728
|
+
});
|
|
33729
|
+
const grantFiles = Effect.fn("LocalApplicationFiles.grants")(function* (subscriptionId, occurrences) {
|
|
33730
|
+
for (const [token, grant] of grants) if (grant.subscriptionId === subscriptionId) grants.delete(token);
|
|
33731
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
33732
|
+
for (const occurrence of occurrences) {
|
|
33733
|
+
if (tokens.has(occurrence.file.id)) continue;
|
|
33734
|
+
const token = yield* ids.generate(FileGrantToken);
|
|
33735
|
+
tokens.set(occurrence.file.id, token);
|
|
33736
|
+
grants.set(token, {
|
|
33737
|
+
subscriptionId,
|
|
33738
|
+
file: occurrence.file
|
|
33739
|
+
});
|
|
33740
|
+
}
|
|
33741
|
+
return occurrences.map((occurrence) => ({
|
|
33742
|
+
...occurrence,
|
|
33743
|
+
url: `${fileGrantUrlPrefix}${tokens.get(occurrence.file.id)}`
|
|
33744
|
+
}));
|
|
33745
|
+
});
|
|
33746
|
+
const reconcile = Effect.gen(function* () {
|
|
33747
|
+
const rows = yield* sql`
|
|
33748
|
+
SELECT fields FROM documents
|
|
33749
|
+
`;
|
|
33750
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
33751
|
+
for (const row of rows) {
|
|
33752
|
+
const fields = yield* Schema.decodeEffect(JsonObjectString)(row.fields).pipe(Effect.orDie);
|
|
33753
|
+
for (const occurrence of encodedFileOccurrencesOf(fields)) referenced.add(occurrence.file.id);
|
|
33754
|
+
}
|
|
33755
|
+
const stored = yield* sql`
|
|
33756
|
+
SELECT fileId, name, format, size, ready FROM applicationFiles
|
|
33757
|
+
`;
|
|
33758
|
+
for (const file of stored) {
|
|
33759
|
+
if (referenced.has(file.fileId)) continue;
|
|
33760
|
+
yield* fileSystem.remove(objectPath(file.fileId), { force: true }).pipe(Effect.orDie);
|
|
33761
|
+
yield* sql`DELETE FROM applicationFiles WHERE fileId = ${file.fileId}`;
|
|
33762
|
+
}
|
|
33763
|
+
});
|
|
33764
|
+
return LocalApplicationFiles.of({
|
|
33765
|
+
admit: (...args) => admit(...args).pipe(Effect.orDie),
|
|
33766
|
+
grants: grantFiles,
|
|
33767
|
+
readGrant: (token) => {
|
|
33768
|
+
const grant = grants.get(token);
|
|
33769
|
+
return grant === void 0 ? Effect.succeed(Option.none()) : fileSystem.readFile(objectPath(grant.file.id)).pipe(Effect.orDie, Effect.map((bytes) => Option.some({
|
|
33770
|
+
file: grant.file,
|
|
33771
|
+
bytes
|
|
33772
|
+
})));
|
|
33773
|
+
},
|
|
33774
|
+
reconcile: reconcile.pipe(Effect.orDie),
|
|
33775
|
+
releaseGrants: (subscriptionId) => Effect.sync(() => {
|
|
33776
|
+
for (const [token, grant] of grants) if (grant.subscriptionId === subscriptionId) grants.delete(token);
|
|
33777
|
+
}),
|
|
33778
|
+
releasePreparation: (invocationId) => Effect.suspend(() => {
|
|
33779
|
+
if (!preparations.delete(invocationId)) return Effect.void;
|
|
33780
|
+
return reconcile.pipe(Effect.orDie);
|
|
33781
|
+
}),
|
|
33782
|
+
prepare: (...args) => prepare(...args).pipe(Effect.orDie),
|
|
33783
|
+
upload: (...args) => upload(...args).pipe(Effect.orDie)
|
|
33784
|
+
});
|
|
33785
|
+
}));
|
|
33786
|
+
//#endregion
|
|
32569
33787
|
//#region src/dev-runtime/sync.ts
|
|
32570
33788
|
const localSyncIdentity = {
|
|
32571
33789
|
appId: AppId.make("app_000000000000000000000001"),
|
|
@@ -32643,6 +33861,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
32643
33861
|
const runtime = yield* FunctionRuntime;
|
|
32644
33862
|
const invalidation = yield* QueryInvalidation;
|
|
32645
33863
|
const mutationReplay = yield* MutationReplay;
|
|
33864
|
+
const files = yield* LocalApplicationFiles;
|
|
32646
33865
|
const subscriptions = yield* Ref.make(HashMap.empty());
|
|
32647
33866
|
const dependencyIndex = makeDependencyIndex();
|
|
32648
33867
|
const invocationFibers = yield* FiberSet.make();
|
|
@@ -32681,11 +33900,16 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
32681
33900
|
if (!(yield* isActive(subscriptionId, subscription)) || subscription.refreshGeneration !== refreshGeneration) return;
|
|
32682
33901
|
dependencyIndex.record(subscriptionId, result.dependencies);
|
|
32683
33902
|
if ((yield* invalidation.latestRevision) > result.observedRevision) continue;
|
|
32684
|
-
yield*
|
|
33903
|
+
const granted = yield* files.grants(subscriptionId, result.files ?? []);
|
|
33904
|
+
const snapshot = {
|
|
32685
33905
|
type: "Snapshot",
|
|
32686
33906
|
id: subscriptionId,
|
|
32687
33907
|
result: result.result,
|
|
32688
33908
|
revision: result.observedRevision
|
|
33909
|
+
};
|
|
33910
|
+
yield* deliver(granted.length === 0 ? snapshot : {
|
|
33911
|
+
...snapshot,
|
|
33912
|
+
files: granted
|
|
32689
33913
|
});
|
|
32690
33914
|
return;
|
|
32691
33915
|
}
|
|
@@ -32713,6 +33937,10 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
32713
33937
|
yield* PubSub.take(invalidations).pipe(Effect.flatMap((event) => event.type === "All" ? refreshAll() : refreshAffected(event.invalidations)), Effect.forever, Effect.forkScoped);
|
|
32714
33938
|
const handleSubscribe = Effect.fn("SyncServer.handleSubscribe")(function* (message) {
|
|
32715
33939
|
const operation = operationForSubscription(message.id);
|
|
33940
|
+
if (encodedFileOccurrencesOf(message.args).length > 0) {
|
|
33941
|
+
yield* send(protocolError("InvalidArguments", "Queries cannot take files as arguments.", operation));
|
|
33942
|
+
return;
|
|
33943
|
+
}
|
|
32716
33944
|
const current = yield* Ref.get(subscriptions);
|
|
32717
33945
|
if (HashMap.has(current, message.id)) {
|
|
32718
33946
|
yield* send(protocolError("DuplicateOperationId", `Subscription ${message.id} already exists.`, operation));
|
|
@@ -32739,13 +33967,20 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
32739
33967
|
type: "Invocation",
|
|
32740
33968
|
id: message.id
|
|
32741
33969
|
};
|
|
33970
|
+
if (!(yield* files.admit(message.id, message.function, message.args).pipe(Effect.as(true), Effect.catchCause((cause) => send(protocolError("InvalidArguments", `The mutation files could not be admitted: ${String(cause)}`, operation)).pipe(Effect.as(false)))))) {
|
|
33971
|
+
yield* files.releasePreparation(message.id);
|
|
33972
|
+
return;
|
|
33973
|
+
}
|
|
32742
33974
|
const prepared = yield* runtime.prepare(message.function, "Mutation", message.args).pipe(Effect.catchTags({
|
|
32743
33975
|
FunctionUnavailable: (error) => sendResolutionError(operation, error),
|
|
32744
33976
|
InvalidArguments: (error) => sendResolutionError(operation, error),
|
|
32745
33977
|
UnknownFunction: (error) => sendResolutionError(operation, error),
|
|
32746
33978
|
WrongFunctionKind: (error) => sendResolutionError(operation, error)
|
|
32747
33979
|
}));
|
|
32748
|
-
if (prepared === void 0)
|
|
33980
|
+
if (prepared === void 0) {
|
|
33981
|
+
yield* files.releasePreparation(message.id);
|
|
33982
|
+
return;
|
|
33983
|
+
}
|
|
32749
33984
|
const input = canonicalInvocationInput(localSyncIdentity.deploymentId, "Mutation", message.function, message.args);
|
|
32750
33985
|
yield* mutationReplay.execute(message.id, input, prepared.execute.pipe(Effect.flatMap((result) => result.type === "Mutation" ? Effect.succeed(result) : Effect.die("Expected mutation")))).pipe(Effect.flatMap((result) => {
|
|
32751
33986
|
if (!("committedRevision" in result)) return send({
|
|
@@ -32763,12 +33998,23 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
32763
33998
|
invalidations: result.invalidations,
|
|
32764
33999
|
committedRevision: result.committedRevision
|
|
32765
34000
|
})));
|
|
32766
|
-
}), Effect.catchTags({ InvocationIdConflict: () => send(protocolError("InvocationIdConflict", `Invocation ${message.id} was already used with different inputs.`, operation)) }), Effect.catchCause((cause) => Effect.logError("A mutation invocation fiber failed.").pipe(Effect.annotateLogs({
|
|
34001
|
+
}), Effect.catchTags({ InvocationIdConflict: () => send(protocolError("InvocationIdConflict", `Invocation ${message.id} was already used with different inputs.`, operation)) }), Effect.ensuring(files.releasePreparation(message.id)), Effect.catchCause((cause) => Effect.logError("A mutation invocation fiber failed.").pipe(Effect.annotateLogs({
|
|
32767
34002
|
cause,
|
|
32768
34003
|
function: message.function,
|
|
32769
34004
|
requestId: message.id
|
|
32770
34005
|
}))), FiberSet.run(invocationFibers), Effect.asVoid);
|
|
32771
34006
|
});
|
|
34007
|
+
const handlePrepareMutation = Effect.fn("SyncServer.handlePrepareMutation")(function* (message) {
|
|
34008
|
+
const operation = {
|
|
34009
|
+
type: "Invocation",
|
|
34010
|
+
id: message.id
|
|
34011
|
+
};
|
|
34012
|
+
yield* files.prepare(message.id, message.function, message.args, message.files).pipe(Effect.flatMap((uploads) => send({
|
|
34013
|
+
type: "MutationPrepared",
|
|
34014
|
+
id: message.id,
|
|
34015
|
+
uploads
|
|
34016
|
+
})), Effect.catchCause((cause) => send(protocolError("InvalidArguments", `The mutation files could not be prepared: ${String(cause)}`, operation))));
|
|
34017
|
+
});
|
|
32772
34018
|
const handleMessage = Effect.fn("SyncServer.handleMessage")(function* (text) {
|
|
32773
34019
|
const message = yield* Schema.decodeEffect(ClientMessageJson)(text).pipe(Effect.catch(() => send(protocolError("InvalidMessage", "The WebSocket frame is not valid Ignotum JSON."))));
|
|
32774
34020
|
if (message === void 0) return;
|
|
@@ -32779,6 +34025,10 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
32779
34025
|
case "Unsubscribe":
|
|
32780
34026
|
dependencyIndex.remove(message.id);
|
|
32781
34027
|
yield* Ref.update(subscriptions, HashMap.remove(message.id));
|
|
34028
|
+
yield* files.releaseGrants(message.id);
|
|
34029
|
+
return;
|
|
34030
|
+
case "PrepareMutation":
|
|
34031
|
+
yield* handlePrepareMutation(message);
|
|
32782
34032
|
return;
|
|
32783
34033
|
case "Invoke":
|
|
32784
34034
|
yield* handleInvoke(message);
|
|
@@ -32788,7 +34038,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
32788
34038
|
yield* socket.runString((text) => messageSemaphore.withPermits(1)(handleMessage(text)), { onOpen: send({
|
|
32789
34039
|
type: "Handshake",
|
|
32790
34040
|
...localSyncIdentity
|
|
32791
|
-
}).pipe(Effect.orDie) });
|
|
34041
|
+
}).pipe(Effect.orDie) }).pipe(Effect.ensuring(Ref.get(subscriptions).pipe(Effect.flatMap((active) => Effect.forEach(HashMap.keys(active), (subscriptionId) => files.releaseGrants(subscriptionId))), Effect.asVoid)));
|
|
32792
34042
|
});
|
|
32793
34043
|
var SyncHandlers = class extends Context.Service()("ignotum/dev-runtime/sync/SyncHandlers") {};
|
|
32794
34044
|
const makeHandlersLayer = (server, appDirectory, databasePath) => {
|
|
@@ -32796,7 +34046,7 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
|
|
|
32796
34046
|
const persistenceLayer = Layer.merge(LocalDatabase.layer, mutationReplayLayer).pipe(Layer.provideMerge(DevelopmentDatabase.layer), Layer.provide(Layer.merge(idGeneratorLayer, sqliteLayer)));
|
|
32797
34047
|
const executorLayer = FunctionExecutor.layer.pipe(Layer.provide(persistenceLayer), Layer.provide(idGeneratorLayer));
|
|
32798
34048
|
const devFunctionRuntimeLayer = functionRuntimeLayer.pipe(Layer.provide(Layer.merge(FunctionRegistry.layer(server, appDirectory).pipe(Layer.provide(layer$1)), executorLayer)));
|
|
32799
|
-
const dependencies = Layer.mergeAll(devFunctionRuntimeLayer, queryInvalidationLayer, persistenceLayer, layer$1);
|
|
34049
|
+
const dependencies = Layer.mergeAll(devFunctionRuntimeLayer, queryInvalidationLayer, persistenceLayer, layer$1, localApplicationFilesLayer(appDirectory).pipe(Layer.provideMerge(DevelopmentDatabase.layer), Layer.provide(Layer.mergeAll(idGeneratorLayer, sqliteLayer, layer$1))));
|
|
32800
34050
|
return Layer.effect(SyncHandlers, Effect.gen(function* () {
|
|
32801
34051
|
const invalidation = yield* QueryInvalidation;
|
|
32802
34052
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
@@ -32812,6 +34062,30 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
|
|
|
32812
34062
|
const httpApp = Effect.gen(function* () {
|
|
32813
34063
|
const request = yield* HttpServerRequest$1.HttpServerRequest;
|
|
32814
34064
|
const pathname = new URL(request.url, "http://ignotum.local").pathname;
|
|
34065
|
+
if (pathname.startsWith(fileUploadUrlPrefix)) {
|
|
34066
|
+
if (request.method !== "PUT") return HttpServerResponse.text("Method Not Allowed", { status: 405 });
|
|
34067
|
+
const token = Schema.decodeOption(FileUploadToken)(pathname.slice(fileUploadUrlPrefix.length));
|
|
34068
|
+
if (Option.isNone(token)) return HttpServerResponse.text("Not Found", { status: 404 });
|
|
34069
|
+
const bytes = yield* collectStreamBytes(request.stream, fileLimits.fileBytes, () => /* @__PURE__ */ new Error("The local application file is too large.")).pipe(Effect.orDie);
|
|
34070
|
+
return yield* LocalApplicationFiles.pipe(Effect.flatMap((storage) => storage.upload(token.value, bytes)), Effect.match({
|
|
34071
|
+
onFailure: (error) => HttpServerResponse.text(String(error), { status: 422 }),
|
|
34072
|
+
onSuccess: () => HttpServerResponse.text("Stored", { status: 201 })
|
|
34073
|
+
}));
|
|
34074
|
+
}
|
|
34075
|
+
if (pathname.startsWith(fileGrantUrlPrefix)) {
|
|
34076
|
+
if (request.method !== "GET" && request.method !== "HEAD") return HttpServerResponse.text("Method Not Allowed", { status: 405 });
|
|
34077
|
+
const token = Schema.decodeOption(FileGrantToken)(pathname.slice(fileGrantUrlPrefix.length));
|
|
34078
|
+
if (Option.isNone(token)) return HttpServerResponse.text("Not Found", { status: 404 });
|
|
34079
|
+
const granted = yield* LocalApplicationFiles.pipe(Effect.flatMap((storage) => storage.readGrant(token.value)));
|
|
34080
|
+
if (Option.isNone(granted)) return HttpServerResponse.text("Not Found", { status: 404 });
|
|
34081
|
+
const headers = {
|
|
34082
|
+
"cache-control": "private, no-store",
|
|
34083
|
+
"content-length": String(granted.value.bytes.byteLength),
|
|
34084
|
+
"content-type": fileMimeTypes[granted.value.file.format],
|
|
34085
|
+
"x-content-type-options": "nosniff"
|
|
34086
|
+
};
|
|
34087
|
+
return request.method === "HEAD" ? HttpServerResponse.empty({ headers }) : HttpServerResponse.uint8Array(granted.value.bytes, { headers });
|
|
34088
|
+
}
|
|
32815
34089
|
return yield* HttpServerResponse.json(pathname === syncPath ? {
|
|
32816
34090
|
code: "UpgradeRequired",
|
|
32817
34091
|
message: `Connect to ${syncPath} with WebSocket.`
|
|
@@ -32991,28 +34265,58 @@ const dev$1 = Effect.fn("Dev.run")(function* (options) {
|
|
|
32991
34265
|
return yield* Effect.never;
|
|
32992
34266
|
}));
|
|
32993
34267
|
});
|
|
34268
|
+
//#endregion
|
|
34269
|
+
//#region ../../docs/agent/AGENTS.md?raw
|
|
34270
|
+
var AGENTS_default = "# Ignotum app instructions\n\nThis repository is an Ignotum app. Read `.agents/skills/ignotum/SKILL.md` before changing the\nschema, server functions, generated references, client code, app structure, or development setup.\n\n## Source boundaries\n\n- `server/schema.ts` defines tables and indexes.\n- TypeScript files directly inside `server` define public queries and mutations.\n- `client/index.tsx` default-exports the app definition and root component.\n- `shared` contains code that is safe in both client and server builds.\n- `_generated` is compiler output. Never edit it.\n\nUse `ignotum/server` only for schema authoring. Use generated builders from\n`@/_generated/server.js` in function modules. Use `ignotum/client` and\n`@/_generated/api.js` in client code. Keep the `.js` suffix on generated and aliased imports.\n\nDo not add an HTML entry, framework configuration, Tailwind configuration, API route, direct\ndatabase client, or backend server. Those do not belong to an Ignotum app.\n\n## Before finishing\n\nRun the app's existing checks. When no project-specific command says otherwise, regenerate the\nbindings and typecheck:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nDo not reset the development database unless the user explicitly asks to discard local data.\n";
|
|
34271
|
+
//#endregion
|
|
34272
|
+
//#region ../../docs/agent/skills/ignotum/SKILL.md?raw
|
|
34273
|
+
var SKILL_default = "---\nname: ignotum\ndescription: Build and modify an Ignotum app, including its database, functions, files, Web code, generated references, local workflow, and deployment configuration.\n---\n\n# Work on an Ignotum app\n\nUse this skill for changes to an Ignotum app. The bundled references describe the installed\nIgnotum release. Prefer them to assumptions based on another backend or UI framework.\n\n## Inspect before editing\n\nRead the files that define the part of the app you are changing:\n\n- `server/schema.ts` for tables, fields, indexes, and generated document types;\n- TypeScript files directly inside `server` for queries and mutations;\n- `client/index.tsx` and imported client modules for the UI;\n- `shared` for code imported by both sides;\n- `package.json` and `tsconfig.json` for the available scripts and aliases.\n\nInspect `_generated` when you need the exact generated type or function address. Never edit it.\nGenerated files can lag behind source until `ignotum codegen` or the dev server runs.\n\nRead the smallest relevant set of references before changing code. If a task crosses several\nareas, read each corresponding page completely.\n\n## Choose the reference\n\n| Task | Reference |\n| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |\n| Create or understand an app | [Quickstart](references/quickstart.md) and [Project structure](references/project-structure.md) |\n| Change tables, fields, or IDs | [Schema](references/schema.md) and [Values](references/values.md) |\n| Define or query an index | [Indexes](references/indexes.md) |\n| Read or paginate data | [Reading data](references/reading-data.md) and [Pagination](references/pagination.md) |\n| Write data | [Writing data](references/writing-data.md) |\n| Add or call a query | [Functions](references/functions.md) and [Queries](references/queries.md) |\n| Add or call a mutation | [Functions](references/functions.md) and [Mutations](references/mutations.md) |\n| Define or handle application errors | [Errors](references/errors.md) |\n| Accept, store, return, or use application files | [Files](references/files.md) |\n| Change the app entry or Preact code | [Web](references/web.md), [App entry](references/app-entry.md), and [Preact](references/preact.md) |\n| Change styles, the icon, or public assets | [Styling](references/styling.md) and [Public assets](references/public-assets.md) |\n| Reason about transactions, ordering, retries, or live updates | [Guarantees](references/guarantees.md) |\n| Run, configure, or reset local development | [Local development](references/local-development.md) |\n| Update or inspect generated bindings | [Generated code](references/generated-code.md) |\n| Deploy or change the app link | [Deploy an app](references/deploy-app.md) and [Deployment model](references/deployment-model.md) |\n| Look up commands and flags | [CLI](references/cli.md) |\n| Check hosted capacity or argument sizes | [Limits](references/limits.md) |\n| Build deployment tooling against `api.ignotum.cloud` | [HTTP API](references/api.md) |\n\n## Preserve the app model\n\nAn Ignotum app has a fixed division of responsibility:\n\n- Define the schema only in `server/schema.ts` with `defineSchema` from `ignotum/server`.\n- Define functions in `.ts` files directly inside `server`. Nested files do not become API modules.\n- Import schema-bound `query`, `mutation`, `values`, and `Result` from\n `@/_generated/server.js`.\n- Import generated client references from `@/_generated/api.js`.\n- Import JSX, hooks, UI types, `app`, and query or mutation hooks from `ignotum/client`.\n- Put cross-boundary code in `shared` only when it uses APIs available to both the browser and\n server function environment.\n\nDo not add React, a separate Preact package, a router, an HTML entry, Vite configuration, Tailwind\nconfiguration, an ORM, a direct database connection, custom API routes, or a second backend unless\nthe user explicitly asks to leave the Ignotum application model. Authentication, actions,\nschedules, and workflows are not current Ignotum features.\n\nClient code cannot import server modules, server generated bindings, Node built-ins, or\n`ignotum/server`. Server code cannot import client modules, client generated references, Node\nbuilt-ins, or `ignotum/client`. Dynamic imports and native add-ons do not belong in server\nfunctions.\n\n## Change the schema carefully\n\nUse validators for every stored field. They provide runtime validation as well as TypeScript types.\nUse table-specific IDs for relationships and declare an index for ordered or filtered access that\nmust avoid a full table read.\n\nBefore changing a deployed app's fields, check\n[Deployment model](references/deployment-model.md). After the first\nactivation, hosted deployment requires the same table names, field names, and field validators.\nIndex-only changes are allowed. Do not promise that adding an optional field is compatible.\n\nEvery stored document receives `id`, `createdAt`, and `updatedAt`. Do not declare those fields in\nthe schema or write them through insert, patch, or replace input.\n\n## Write functions through generated builders\n\nHandlers are generator functions. Use `yield*` for database operations and `return` for the public\nvalue. Declare `args`, `returns`, and application `errors` so the client receives accurate types and\nruntime checks.\n\nChoose the function kind by behavior:\n\n- A query reads one committed state and cannot write.\n- A mutation may read and write. Its writes commit together only after a valid success result.\n- Use a declared application error for an expected domain outcome. Do not throw it or convert it to\n a generic exception.\n\nUse `find` when a missing document is an ordinary branch. Use `get` when absence should fail with\n`DocumentNotFound`. `patch`, `replace`, and `delete` fail with the same error when their target is\nmissing. Check with `find` first when a missing write target should be ignored.\n\nIndexes order by their declared fields, then `createdAt`, then `id`. Add equality bounds for a\nleading prefix before using a range bound on the next field. Do not emulate an indexed filter by\ncollecting an unbounded table unless the table is intentionally small.\n\n## Use the client result model\n\n`useQuery` returns `Pending`, a value, or a declared error. Render each reachable state with\n`Result.match`. Pass `Query.skip` when the arguments are not ready instead of inventing placeholder\narguments.\n\n`useMutation` returns a stable typed function. Its promise resolves to the declared result and can\nreject for client or protocol failures. Internal function failures use the result's\n`InternalServerError` branch. A resolved success means its commit is complete, but subscribed\nqueries may update afterward.\n\nFor long ordered lists, use `usePaginatedQuery` with a function that calls `.paginate(...)`. Keep\ndocument IDs as rendering keys. Loaded pages remain live and do not form a frozen historical\nsnapshot.\n\nThe required `client/index.tsx` must default-export one direct `app({ title, component })` call.\nImport `app` by that name from `ignotum/client`, use a non-empty quoted title, and provide only the\n`title` and `component` properties. The optional icon path is exactly `client/icon.svg`. Put\nsupported public files in top-level `public`, not `client/public`.\n\n## Verify the change\n\nUse existing app scripts when they are stricter. Otherwise run:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nRun focused tests when the app has them. For behavior that depends on live results, also run the dev\nserver and exercise the affected query or mutation from the Web part.\n\nTreat `.ignotum/dev/state.db` as user data. Stop the dev server before `npx ignotum dev db reset`,\nand reset it only when the user asked for a clean local database. Treat `.ignotum/app.json` as the\nhosted app link; do not delete or rewrite it to solve a deployment problem without confirming that\nthe app should be relinked.\n";
|
|
34274
|
+
//#endregion
|
|
34275
|
+
//#region src/cli/agent-files.ts
|
|
34276
|
+
const references = [
|
|
34277
|
+
["api.md", "---\ntitle: HTTP API\ndescription: Use the authenticated API at api.ignotum.cloud.\nsection: Reference\nslug: api\norder: 73\n---\n\n# HTTP API\n\nThe public API at `https://api.ignotum.cloud` creates apps and manages immutable deployments. The\n`ignotum deploy` command is the normal client for these endpoints. Use the HTTP API directly when\nbuilding deployment tooling or checking an app after an interrupted command.\n\nThe API does not expose application queries and mutations over HTTP. Deployed clients call those\nfunctions through the generated client API.\n\n## Authentication\n\nEvery route except `GET /health` requires a bearer token:\n\n```http\nAuthorization: Bearer <IGNOTUM_API_TOKEN>\n```\n\nTreat the token as an account credential. Do not include it in client code or send it from a\ndeployed app. The API does not enable browser CORS, so call it from a CLI, CI job, or other trusted\nserver environment.\n\nJSON requests require `Content-Type: application/json`. A JSON body may be at most 1 MiB. Responses\nuse JSON unless a route is not found.\n\n## Common values\n\nIgnotum IDs are opaque strings. Their prefixes identify the kind:\n\n```text\napp_000000000000000000000001\ndep_000000000000000000000001\nteam_000000000000000000000001\n```\n\nEach prefix is followed by 24 lowercase letters or digits. Do not parse or generate these IDs.\n\nTimestamps such as `createdAt` and `readyAt` are Unix time in milliseconds.\n\nAn app object has this shape:\n\n```json\n{\n \"id\": \"app_000000000000000000000001\",\n \"teamId\": \"team_000000000000000000000001\",\n \"slug\": \"team-todos\",\n \"activeDeploymentId\": \"dep_000000000000000000000001\",\n \"deploymentGeneration\": 3,\n \"createdAt\": 1787925600000\n}\n```\n\n`activeDeploymentId` is `null` before the first activation. `deploymentGeneration` starts at 0 and\nincreases when a deployment becomes active.\n\n## Health\n\n```http\nGET /health\n```\n\nA healthy API returns `200`:\n\n```json\n{ \"service\": \"api\", \"status\": \"ok\" }\n```\n\nAn infrastructure check failure returns `503` with `status` set to `unavailable`. This route does\nnot require authentication.\n\n## Create an app\n\n```http\nPOST /v1/apps\nContent-Type: application/json\nAuthorization: Bearer <token>\n\n{ \"slug\": \"team-todos\" }\n```\n\nA slug is 1 through 63 lowercase letters, digits, or hyphens. It must start and end with a letter\nor digit. A successful request returns `201` and the new app object.\n\nThis operation is not idempotent. A repeated request for the same slug returns `SlugUnavailable`.\n\n## Get an app\n\nLook up an app by ID:\n\n```http\nGET /v1/apps/app_000000000000000000000001\nAuthorization: Bearer <token>\n```\n\nOr by slug:\n\n```http\nGET /v1/apps/by-slug/team-todos\nAuthorization: Bearer <token>\n```\n\nBoth routes return `200` and the same app shape. The authenticated account must own the app. The\nAPI currently has no route to list apps, rename a slug, or delete an app.\n\n## Deployment flow\n\nA deployment uses four steps:\n\n1. Begin with an inventory.\n2. Upload the canonical `inventory.json` and every file listed by it.\n3. Finalize the deployment.\n4. Activate the ready deployment.\n\nThe deployment is inactive until the last step succeeds.\n\n### Inventory\n\nThe begin request contains a version 1 inventory:\n\n```json\n{\n \"formatVersion\": 1,\n \"files\": [\n {\n \"path\": \"client/shell.html\",\n \"size\": 128,\n \"sha256\": \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"kind\": \"ClientShell\",\n \"contentType\": \"text/html; charset=utf-8\"\n }\n ]\n}\n```\n\nEach file entry has:\n\n| Field | Meaning |\n| ----------------- | ---------------------------------------------------------------------- |\n| `path` | Relative artifact path with `/` separators and no `.` or `..` segment. |\n| `size` | Byte length as a non-negative integer. |\n| `sha256` | Lowercase 64-character SHA-256 digest. |\n| `kind` | The artifact role listed below. |\n| `contentType` | MIME type stored with the file. |\n| `contentEncoding` | Optional encoding metadata. |\n\nThe defined kinds are `ClientAsset`, `ClientDocument`, `ClientManifest`, `ClientPublicFile`,\n`ClientShell`, `FunctionBundle`, `ServerManifest`, `SourceMap`, and `SchemaSnapshot`.\n\nFiles must be sorted by `path`. The inventory must contain `client/manifest.json`,\n`client/shell.html`, `server/manifest.json`, and `server/schema.json`. Paths and kinds must agree,\nand every manifest reference must match the inventory. `inventory.json` is not listed because it\ncannot contain its own hash.\n\nThe byte limits are in [Limits](limits.md). The public `ignotum` package does not expose an artifact\nbuilder or a build-only command. The formats below let deployment tooling inspect and transport an\nartifact emitted by the CLI. Building function bundles with another compiler is not a supported\npublic extension point.\n\nCanonical `inventory.json` is UTF-8 JSON with object keys sorted lexicographically, array order\npreserved, no insignificant whitespace, and one trailing line feed. Its upload headers contain the\nbyte length and SHA-256 digest of those exact bytes.\n\n### Client manifest\n\n`client/manifest.json` connects hosted URL paths to client files:\n\n```json\n{\n \"formatVersion\": 1,\n \"shell\": {\n \"path\": \"client/shell.html\",\n \"size\": 128,\n \"sha256\": \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"\n },\n \"routes\": [\n {\n \"pathname\": \"/manual.pdf\",\n \"artifact\": {\n \"path\": \"client/routes/manual.pdf\",\n \"size\": 4096,\n \"sha256\": \"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789\"\n }\n }\n ]\n}\n```\n\nThe shell reference must point to `client/shell.html`. Routes must be sorted by `pathname`. A\npathname starts with `/`, uses only letters, digits, `.`, `_`, `~`, `-`, and `/`, and cannot claim\n`/_ignotum`. Each route points to a matching `ClientDocument` or `ClientPublicFile` inventory entry.\nAn artifact can have only one route, and every document or public-file entry must have one.\n\nFiles under `client/assets/` use `ClientAsset`. Files under `client/routes/` use `ClientDocument`\nwhen their name ends in `.html`; AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF files use\n`ClientPublicFile`. The four fixed client paths use their corresponding fixed kinds.\n\n### Server manifest\n\n`server/manifest.json` lists every callable function and the schema snapshot:\n\n```json\n{\n \"formatVersion\": 1,\n \"schema\": {\n \"path\": \"server/schema.json\",\n \"size\": 512,\n \"sha256\": \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"\n },\n \"functions\": [\n {\n \"address\": \"api.todos.list\",\n \"kind\": \"Query\",\n \"bundle\": {\n \"path\": \"server/functions/todos/list-a1b2c3.mjs\",\n \"size\": 2048,\n \"sha256\": \"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789\"\n },\n \"sourceMap\": {\n \"path\": \"server/functions/todos/list-a1b2c3.mjs.map\",\n \"size\": 1024,\n \"sha256\": \"123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0\"\n }\n }\n ]\n}\n```\n\nFunction addresses have the form `api.<module>.<export>`, where both names are TypeScript\nidentifiers. `kind` is `Query` or `Mutation`. Addresses and referenced paths must be unique. A\nbundle ends in `.mjs`; its source map is the same path followed by `.map`. Every `server/` file is\nthe manifest, schema, or one referenced bundle or source map.\n\n### Schema snapshot\n\n`server/schema.json` is version 2:\n\n```json\n{\n \"formatVersion\": 2,\n \"tables\": [\n {\n \"name\": \"todos\",\n \"fields\": [\n { \"name\": \"completed\", \"value\": { \"type\": \"boolean\" } },\n { \"name\": \"text\", \"value\": { \"type\": \"string\" } }\n ],\n \"indexes\": [{ \"name\": \"by_completed\", \"fields\": [\"completed\"] }]\n }\n ]\n}\n```\n\nSort tables and fields by name. Sort indexes by name and preserve each index's declared field\norder. A value descriptor uses one of these recursive forms:\n\n```text\n{ \"type\": \"boolean\" | \"date\" | \"integer\" | \"never\" | \"null\" | \"number\" | \"string\" }\n{ \"type\": \"id\", \"table\": string }\n{ \"type\": \"literal\", \"value\": string | finite number | boolean }\n{ \"type\": \"literals\", \"values\": Array<string | finite number | boolean> }\n{ \"type\": \"array\" | \"nullable\" | \"optional\" | \"record\", \"value\": descriptor }\n{ \"type\": \"union\", \"members\": Array<descriptor> }\n{ \"type\": \"object\", \"fields\": Array<{ \"name\": string, \"value\": descriptor }> }\n{ \"type\": \"error\", \"tag\": string, \"fields\": Array<{ \"name\": string, \"value\": descriptor }> }\n```\n\nSort object and error fields by name. This snapshot must describe the same schema used to build the\nfunction bundles. After the first activation, table and field descriptors must match the active\nsnapshot exactly; only the `indexes` arrays may change.\n\n### Begin\n\n```http\nPOST /v1/apps/{appId}/deployments\nContent-Type: application/json\nAuthorization: Bearer <token>\n\n{ \"inventory\": { \"formatVersion\": 1, \"files\": [] } }\n```\n\nThe abbreviated body above shows the envelope only. Send a complete valid inventory. Success\nreturns `201` and a deployment object:\n\n```json\n{\n \"id\": \"dep_000000000000000000000002\",\n \"appId\": \"app_000000000000000000000001\",\n \"status\": \"Uploading\",\n \"inventory\": { \"formatVersion\": 1, \"files\": [] },\n \"createdAt\": 1787925601000,\n \"readyAt\": null\n}\n```\n\nThe response repeats the complete accepted inventory. This operation is not idempotent. Repeating\nit creates another deployment.\n\n### Upload a file\n\nUpload each path as one percent-encoded route component. For example,\n`client/shell.html` becomes `client%2Fshell.html`:\n\n```http\nPUT /v1/apps/{appId}/deployments/{deploymentId}/files/client%2Fshell.html\nAuthorization: Bearer <token>\nContent-Length: 128\nX-Ignotum-Sha256: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n\n<128 raw bytes>\n```\n\n`Content-Length` and `X-Ignotum-Sha256` must match the accepted inventory entry. The body is the raw\nfile, not JSON. Upload the canonical `inventory.json` through the same route even though it is not\nin the `files` array.\n\nA new file returns:\n\n```json\n{ \"status\": \"Stored\" }\n```\n\nUploading the same path, bytes, and metadata again is safe and returns `AlreadyStored`. Different\ncontent at an existing path returns `DeploymentFileConflict`; deployment files are immutable.\n\n### Finalize\n\n```http\nPOST /v1/apps/{appId}/deployments/{deploymentId}/finalize\nAuthorization: Bearer <token>\n```\n\nThe request has no body. Finalization checks that every expected file exists, no extra file exists,\nall metadata matches, and the client and server manifests are valid. Success returns `200` with the\ndeployment status changed to `Ready` and `readyAt` set.\n\nFinalizing an already ready deployment is safe and returns its current record.\n\n### Activate\n\n```http\nPOST /v1/apps/{appId}/deployments/{deploymentId}/activate\nAuthorization: Bearer <token>\n```\n\nThe request has no body. Only a ready, unexpired deployment can be activated. Ignotum rejects a\nstored-schema change but permits index changes. See\n[Deployment model](deployment-model.md#schema-compatibility).\n\nSuccess returns `200` with the updated app object. Activating its current deployment again is safe.\nActivation reloads connected app clients.\n\n## Errors\n\nEvery recognized API failure has this body:\n\n```json\n{\n \"code\": \"SlugUnavailable\",\n \"message\": \"The app slug is unavailable.\"\n}\n```\n\nTreat `code` as the machine-readable value. Messages may change.\n\n| Status | Codes |\n| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `400` | `InvalidRequest`, `DeploymentFileUnexpected` |\n| `401` | `Unauthorized` |\n| `403` | `AppAccessDenied` |\n| `404` | `AppNotFound`, `DeploymentNotFound` |\n| `409` | `SlugUnavailable`, `DeploymentNotUploading`, `DeploymentFileConflict`, `DeploymentIncomplete`, `DeploymentNotReady`, `SchemaIncompatible`, `ActivationConflict` |\n| `503` | `ApiUnavailable` |\n\n`ApiUnavailable` does not prove whether a state-changing request reached its final step. Read the\napp before deciding how to recover from an unconfirmed activation. Do not automatically retry\n`POST /v1/apps` or the begin-deployment request because either may create a second resource.\n"],
|
|
34278
|
+
["app-entry.md", "---\ntitle: App entry\ndescription: Define the Web title and root component in client/index.tsx.\nsection: Web\nslug: app-entry\norder: 41\n---\n\n# App entry\n\nThe required `client/index.tsx` file names the page and supplies its root component:\n\n```tsx\nimport { app } from \"ignotum/client\";\n\nfunction App() {\n return <main>My app</main>;\n}\n\nexport default app({\n title: \"My app\",\n component: App,\n});\n```\n\nKeep the entry in this form:\n\n- Import `app` by name from `ignotum/client`, without an alias.\n- Default-export one direct `app(...)` call.\n- Pass one object with exactly `title` and `component`.\n- Write both keys as unquoted properties, without a spread.\n- Use a non-empty quoted string literal for `title`.\n\nThe component can import and render other client modules.\n\nIgnotum does not use an `App.tsx` entry convention. The entry is `client/index.tsx`, and its default\nexport is the `app(...)` definition.\n\nNext: [Preact](preact.md)\n"],
|
|
34279
|
+
["cli.md", "---\ntitle: CLI\ndescription: Look up Ignotum commands, flags, and environment variables.\nsection: Reference\nslug: cli\norder: 72\n---\n\n# CLI\n\nRun commands through the local or downloaded `ignotum` package:\n\n```sh\nnpx ignotum <command>\n```\n\n## Commands\n\n| Command | Purpose |\n| ---------------------- | ------------------------------------------------------------------ |\n| `ignotum new <path>` | Create an app in an empty directory. |\n| `ignotum install` | Install dependencies with pnpm, or npm if pnpm is absent. |\n| `ignotum codegen` | Update files under `_generated`. |\n| `ignotum dev` | Run the local development server. |\n| `ignotum dev db reset` | Delete the local development database while the server is stopped. |\n| `ignotum deploy` | Build, upload, and activate the app. |\n\nUse `ignotum <command> --help` for the command's generated help.\n\n## `new`\n\n```sh\nnpx ignotum new my-app\nnpx ignotum new . --no-git --no-install\n```\n\nThe target directory must be empty. The command installs dependencies and initializes a Git\nrepository by default. Use `--no-install` or `--no-git` to skip either action.\n\n## `dev`\n\n```sh\nnpx ignotum dev --host 0.0.0.0 --port 3000 --open\n```\n\n| Flag | Default | Meaning |\n| -------- | ----------- | ------------------------------------- |\n| `--host` | `127.0.0.1` | Address used by the local server. |\n| `--port` | `3210` | TCP port from 1 through 65,535. |\n| `--open` | off | Open the app after the server starts. |\n\n## `deploy`\n\nUse `--app <slug>` to create or link the hosted app on the first deployment:\n\n```sh\nnpx ignotum deploy --app my-app\n```\n\nLater deployments read the link from `.ignotum/app.json` and do not need the flag.\n\n## Environment variables\n\n| Variable | Meaning |\n| ------------------- | ------------------------------------------------------ |\n| `IGNOTUM_API_TOKEN` | Account token required by `deploy`. |\n| `IGNOTUM_API_URL` | API base URL. Defaults to `https://api.ignotum.cloud`. |\n\n`IGNOTUM_API_URL` must use HTTPS. A numeric loopback address such as `127.0.0.1` or `[::1]` may\nuse HTTP for local platform development.\n"],
|
|
34280
|
+
["database.md", "---\ntitle: Database overview\nlabel: Overview\ndescription: Understand the persistent data model in an Ignotum app.\nsection: Database\nslug: database\norder: 10\n---\n\n# Database overview\n\nEach Ignotum app has a database described by `server/schema.ts`. A schema contains tables, stored\nfields, and indexes. Every stored document also has a table-specific ID and creation and update\ntimestamps.\n\nQueries and mutations read through `ctx.db`. Only mutations can insert, patch, replace, or delete\ndocuments. All reads and writes in one mutation commit together.\n\n- [Schema](schema.md) defines tables, fields, IDs, and schema changes.\n- [Values](values.md) lists the validators used for stored fields and function contracts.\n- [Reading data](reading-data.md) covers document and table reads.\n- [Writing data](writing-data.md) covers database changes and transaction behavior.\n- [Indexes](indexes.md) covers index definitions, ranges, and ordering.\n- [Pagination](pagination.md) covers cursor-based database and Web pagination.\n\nNext: [Schema](schema.md)\n"],
|
|
34281
|
+
["deploy-app.md", "---\ntitle: Deploy an app\ndescription: Link an app and publish a deployment to Ignotum Cloud.\nsection: Deployments\nslug: deploy\norder: 51\n---\n\n# Deploy an app\n\nSet the API token you were issued:\n\n```sh\nexport IGNOTUM_API_TOKEN=your-token\n```\n\nKeep the token outside the app directory and source control. It can create deployments for every\napp available to its account.\n\n## First deployment\n\nChoose the app slug on the first deployment:\n\n```sh\nnpx ignotum deploy --app my-app\n```\n\nSlugs use lowercase letters, numbers, and hyphens. They are 1 through 63 characters, cannot start\nor end with a hyphen, and must be globally available. The deployed app uses\n`https://my-app.ignotum.app`.\n\nIf the slug already belongs to your account, the CLI links the local directory to that app. If it\nis available, the CLI creates the app. If you omit `--app`, it asks for a slug in an interactive\nterminal.\n\nThe CLI writes the app ID, slug, and API URL to `.ignotum/app.json`. It does not write the token\nthere. The generated `.gitignore` excludes `.ignotum`.\n\n## Later deployments\n\nOnce linked, run:\n\n```sh\nnpx ignotum deploy\n```\n\nThe command updates generated code, builds and validates the complete app, uploads a new\ndeployment, and activates it. There is no separate public build command.\n\nThe active deployment stays unchanged if any of these steps fail. See\n[Deployment model](deployment-model.md) for activation, schema compatibility, and interrupted\ndeployments.\n\nNext: [Deployment model](deployment-model.md)\n"],
|
|
34282
|
+
["deployment-model.md", "---\ntitle: Deployment model\ndescription: Understand activation, compatibility, and failure behavior.\nsection: Deployments\nslug: deployment-model\norder: 52\n---\n\n# Deployment model\n\nIgnotum creates a new immutable deployment for each successful `ignotum deploy` run. The active\ndeployment does not change until the new deployment has uploaded, passed validation, and activated.\n\n## Activation\n\nActivation switches the Web part and functions together. Connected browser sessions reload after\nthey learn that the deployment changed, so a session does not intentionally combine Web code from\none deployment with functions from another.\n\nThe CLI has no rollback command. To return to earlier behavior, deploy that source again as a new\ndeployment.\n\n## Schema compatibility\n\nThe first active deployment fixes the stored table and field schema. Later deployments may change\nWeb code, functions, and indexes. Changing a table name, adding or removing a stored field, or\nchanging a field validator causes activation to fail with `SchemaIncompatible`. The active\ndeployment and stored data remain unchanged.\n\nAdding, removing, renaming, or reordering indexes is supported. Ignotum prepares new indexes before\nthe new deployment starts accepting mutations.\n\n## Failed and interrupted deployments\n\nA build, upload, validation, compatibility, or activation failure leaves the current deployment\nactive.\n\nIf an upload or finalization request loses its connection, the CLI prints the new deployment ID.\nUnfinished uploads expire after 24 hours. If activation cannot be confirmed, use the\n[HTTP API](api.md) to inspect the app's `activeDeploymentId`, or run `ignotum deploy` again to create\nand activate a new deployment.\n\nSee [Guarantees](guarantees.md) for the activation contract and [Limits](limits.md) for deployment\nretention.\n"],
|
|
34283
|
+
["deployments.md", "---\ntitle: Deployments overview\nlabel: Overview\ndescription: Understand what an Ignotum deployment publishes.\nsection: Deployments\nslug: deployments\norder: 50\n---\n\n# Deployments overview\n\nA deployment publishes the complete Ignotum app. It contains the Web part, functions, schema, and\npublic assets. Ignotum activates these parts together after the complete deployment passes its\nchecks.\n\nUse `ignotum deploy` to link a local project to a hosted app and publish it. A failed build, upload,\nvalidation, or activation does not replace the active deployment.\n\n- [Deploy an app](deploy-app.md) covers the command, first-time linking, and later deployments.\n- [Deployment model](deployment-model.md) covers activation, schema compatibility, and interrupted\n deployments.\n- [Limits](limits.md) lists deployment size and retention limits.\n\nNext: [Deploy an app](deploy-app.md)\n"],
|
|
34284
|
+
["errors.md", "---\ntitle: Errors\ndescription: Define, return, and handle typed application errors.\nsection: Functions\nslug: errors\norder: 24\n---\n\n# Errors\n\nFunctions can return typed application errors to the client. Ignotum keeps runtime failures and\nthrown exceptions separate from those public errors.\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\nIn a mutation, an application error rolls back every write from that invocation. In a query, it\nbecomes the settled failure for the subscription at the revision the query observed.\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\nAn internal failure also rolls back a mutation. It does not become one of the application's\ndeclared errors, even if a thrown value happens to have the same shape.\n\n## Web matching\n\nThe client `Result.match` separates `error` and `internalError`. `error` handles only errors declared\nby the function. `internalError` handles the reserved platform error. If `internalError` is absent,\nmatching that result throws the error to the nearest UI error boundary.\n\nProtocol and connection failures are outside the application `Result`. A mutation promise rejects\nfor an operation-scoped protocol failure. A query hook throws a subscription infrastructure\nfailure during rendering so the UI error boundary can handle it.\n"],
|
|
34285
|
+
["files.md", "---\ntitle: Files\ndescription: Validate, store, return, and use application files.\nsection: Files\nslug: files\norder: 30\n---\n\n# Files\n\nApplication files are database values. A Web form can pass a browser `File` to a mutation. The\nmutation stores its validated file value in a document, and a query can return it to Web code.\n\n## Declare a file value\n\nUse `values.file` in `server/schema.ts`:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n profiles: table({\n name: values.string(),\n avatar: values.optional(values.file({ formats: [\"jpeg\", \"png\", \"webp\"], maxBytes: 2_000_000 })),\n }),\n}));\n```\n\n`formats` must be a non-empty subset of `jpeg`, `png`, `webp`, `avif`, and `gif`. `maxBytes` is\noptional and cannot exceed the 10 MiB per-file limit. PDF, audio, video, SVG, archives, code, and\nexecutables are not accepted as application files.\n\n## Accept and store a file\n\nA mutation can accept the same file validator and store the result:\n\n```ts\nimport { mutation, values } from \"@/_generated/server.js\";\n\nexport const setAvatar = mutation({\n args: {\n profileId: values.id(\"profiles\"),\n avatar: values.file({ formats: [\"jpeg\", \"png\", \"webp\"], maxBytes: 2_000_000 }),\n },\n\n handler: function* (ctx, args) {\n yield* ctx.db.patch(\"profiles\", args.profileId, { avatar: args.avatar });\n },\n});\n```\n\nThe handler receives `name`, `format`, `mimeType`, and `size`, not the file bytes. Hosted functions\ncannot open or inspect file contents. Ignotum validates the file before invoking the mutation.\n\nMutation returns and application errors cannot contain files. Query arguments cannot contain\nfiles.\n\nIn Web code, pass a native `File` to the generated mutation caller:\n\n```tsx\nimport { Result, useMutation } from \"ignotum/client\";\nimport { api } from \"@/_generated/api.js\";\nimport type { Id } from \"@/_generated/types.js\";\n\nfunction AvatarForm({ profileId }: { profileId: Id<\"profiles\"> }) {\n const setAvatar = useMutation(api.profiles.setAvatar);\n\n const change = (event: Event) => {\n const input = event.currentTarget as HTMLInputElement;\n const avatar = input.files?.[0];\n if (avatar === undefined) return;\n\n void setAvatar({ profileId, avatar }).then(\n Result.match({\n value: () => console.log(\"Saved\"),\n error: () => console.log(\"Could not save\"),\n }),\n );\n };\n\n return <input type=\"file\" accept=\"image/jpeg,image/png,image/webp\" onChange={change} />;\n}\n```\n\nThe browser's `File.type` must identify an allowed format. Ignotum also verifies the file bytes.\n\n## Return and use a file\n\nQueries can return file fields, including nested files and complete documents:\n\n```ts\nexport const get = query({\n args: { id: values.id(\"profiles\") },\n returns: values.doc(\"profiles\"),\n\n handler: function* (ctx, args) {\n return yield* ctx.db.get(\"profiles\", args.id);\n },\n});\n```\n\nUse `Files.url` on the current query value:\n\n```tsx\nimport { Files, Result, useQuery } from \"ignotum/client\";\n\nconst profile = useQuery(api.profiles.get, { id: profileId });\n\nreturn Result.match(profile, {\n pending: () => <span>Loading...</span>,\n value: (value) =>\n value.avatar === undefined ? (\n <span>No avatar</span>\n ) : (\n <img src={Files.url(value.avatar)} alt=\"\" />\n ),\n error: () => <span>Unavailable</span>,\n});\n```\n\nThe returned same-origin URL works in images, links, CSS URLs, and `fetch`. It is not a permanent\nURL. It remains valid while the query subscription and its current result authorize that file.\nUnsubscribing, disconnecting, replacing the result, making the query stale, or deleting the final\ndatabase reference revokes access. Do not save the URL as application data.\n\nCalling `Files.url` on a native file or a value that did not come from an active query throws.\n\n## Copy and remove references\n\nA file value returned by a query can be passed to another compatible mutation argument:\n\n```tsx\nconst copyAvatar = useMutation(api.profiles.copyAvatar);\nvoid copyAvatar({ targetId, avatar: source.avatar });\n```\n\nThe destination validator must allow the file's format and size. Removing one field or document\nremoves that reference. Deleting the final database reference deletes the application file. A\nstale Web value cannot restore it afterward.\n\nDocument changes and file references commit together. A failed mutation does not leave references\npartially changed.\n\n## Limits\n\n- One file is limited to 10 MiB.\n- One mutation may add up to 8 new files and 25 MiB of file data.\n- One app may have up to 100 MiB of uncommitted file data.\n- One query result may contain up to 256 distinct files.\n\nApplication files differ from files in `public`. Public assets belong to a deployment and have\nstable paths. See [Public assets](public-assets.md).\n\nNext: [Deployments](deployments.md)\n"],
|
|
34286
|
+
["functions.md", "---\ntitle: Functions overview\nlabel: Overview\ndescription: Define callable application logic and use its generated API.\nsection: Functions\nslug: functions\norder: 21\n---\n\n# Functions overview\n\nQueries and mutations are callable application functions. Define them in `.ts` files directly\ninside `server`. The file name becomes the generated API module, and each exported function keeps\nits export name:\n\n```text\nserver/todos.ts -> api.todos.list\nserver/users.ts -> api.users.get\n```\n\nFunction module names must be valid TypeScript identifiers. Ignotum ignores `schema.ts`,\n`index.ts`, files ending in `.test.ts` or `.spec.ts`, and names beginning with `_`. Nested files do\nnot become API modules.\n\n## Definitions\n\nImport schema-bound builders from the generated server file:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\n```\n\nKeep the `.js` suffix. The generated definitions provide types and runtime validation for\narguments, successful returns, and application errors.\n\nThe `args` object describes one object parameter. Omit it when the function takes no arguments.\nOmit `returns` only when the handler returns `void`. Ignotum does not infer a runtime validator\nfrom a TypeScript return type.\n\nHandlers are generator functions. Use `yield*` for database and `Result` operations, then use an\nordinary `return` for a successful value. Do not mark a handler `async`.\n\n## Generated API\n\nCode generation writes browser references such as `api.todos.list` to `_generated/api.ts`. Web\ncode passes a reference to `useQuery`, `usePaginatedQuery`, or `useMutation`. The reference carries\nthe function's arguments, result, and errors into the call's TypeScript type.\n\n[Queries](queries.md) and [Mutations](mutations.md) cover each complete workflow.\n\n## Runtime boundary\n\nFunctions can import `_generated/server.ts`, `_generated/types.ts`, other server modules, shared\nmodules, and supported dependencies. They cannot import client files, `_generated/api.ts`,\n`ignotum/client`, Node built-ins, native add-ons, or dynamic imports.\n\nHosted functions have no direct network, filesystem, environment, or process access. Use the APIs\nprovided through the function context.\n\nSee [Values](values.md) for validators, [Errors](errors.md) for the typed error model, and\n[Limits](limits.md) for execution and data limits.\n\nNext: [Queries](queries.md)\n"],
|
|
34287
|
+
["generated-code.md", "---\ntitle: Generated code\ndescription: Understand generated function references, builders, and data types.\nsection: Development\nslug: generated-code\norder: 61\n---\n\n# Generated code\n\nIgnotum generates the bindings that connect the schema, functions, and Web code. The development\nserver updates them before startup and when the relevant source changes.\n\nRun generation directly before a typecheck or an editor session without the development server:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nCode generation writes three files:\n\n| File | Contents |\n| ---------------------- | ------------------------------------------------------- |\n| `_generated/server.ts` | Schema-bound `query`, `mutation`, and `values` exports. |\n| `_generated/api.ts` | Web references such as `api.todos.list`. |\n| `_generated/types.ts` | `DataModel`, `Doc`, and `Id` types. |\n\nDo not edit these files. Ignotum refuses to overwrite a conflicting file that lacks its generated\nheader. Move the conflicting file elsewhere and run `codegen` again.\n\nKeep the `.js` suffix when importing a generated module:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\nimport { api } from \"@/_generated/api.js\";\nimport type { Doc, Id } from \"@/_generated/types.js\";\n```\n\nThe API file changes when function modules or exports change. Server builders and data types change\nwith the schema. See [Functions](functions.md) for function module naming rules.\n"],
|
|
34288
|
+
["guarantees.md", "---\ntitle: Guarantees\ndescription: Understand consistency, atomicity, ordering, retries, and realtime updates.\nsection: Reference\nslug: guarantees\norder: 70\n---\n\n# Guarantees\n\nThis page states the behavior application code can rely on. It also marks the boundaries where\nIgnotum deliberately makes no promise.\n\n## Queries read one committed state\n\nA completed query result corresponds to one committed app-state revision. It does not combine\nreads from different successful mutations.\n\nQueries may run while mutations are committing. If app data changes during a hosted query,\nIgnotum discards that attempt and runs it again. The hosted runtime makes at most four consistency\nattempts within 15 seconds. If changes keep preventing a stable result, the query fails instead of\nreturning a mixed result.\n\nQueries have no write access. A query result can be a success, a declared application error, or an\ninternal failure. All three are tied to the revision observed by that execution.\n\n## Mutations are atomic\n\nA mutation either commits all of its writes or none of them.\n\nIgnotum commits only after the handler returns a value that matches its return validator. These\noutcomes commit no writes:\n\n- a declared application error;\n- an uncaught `DocumentNotFound` or other application error;\n- a thrown exception;\n- an invalid return value or error value;\n- an execution, data, or storage limit failure;\n- an internal platform failure before commit.\n\nDocument changes, index changes, the mutation result used for retries, and the new app-state\nrevision become durable together. A caller that receives a successful mutation result can rely on\nthe writes having committed.\n\n## Mutations are serialized per app\n\nOnly one mutation changes a given app at a time. A mutation sees every mutation that committed\nbefore it began, and it sees its own earlier writes. No other mutation can interleave between its\nreads and writes.\n\nDifferent apps do not share this serialization. Queries may also execute while a mutation is\nrunning, subject to the consistent-query rule above.\n\nDo not infer an ordering between mutation calls started concurrently. If one business action must\nhappen after another, await the first mutation result before starting the second, or combine the\nsteps in one mutation.\n\n## Reconnect retries do not repeat a retained mutation\n\nEach browser mutation call has one invocation ID. The client retains the call while a connection\nis unavailable and sends the same ID, function, and arguments after a reconnect to the same\ndeployment.\n\nIgnotum records the first completed result for that invocation ID. A retry with the same input\nreturns that result without running the handler or applying its writes again. This applies to\nsuccessful mutations and declared application errors.\n\nA reused ID with different input is rejected. An invocation's input includes the deployment,\nfunction, operation kind, and arguments.\n\nHosted retry records last for seven days and are capped at the newest 10,000 results per app. The\ndeduplication guarantee ends when a record expires or is evicted. Use an application-level unique\nkey when duplicate prevention must last longer.\n\nThe client does not replay pending mutations after it detects a different deployment. It reloads\nthe page instead, because running an old call against new server code would change its meaning.\n\n## Realtime queries follow committed writes\n\nWhile a query remains subscribed and the connection can make progress, Ignotum refreshes it after\na successful mutation that may affect one of its reads. Failed mutations cause no refresh because\nthey changed no data.\n\nDependency tracking follows the database operation:\n\n| Read | Writes that may refresh it |\n| ---------------- | ----------------------------------------------------------------------- |\n| `find` or `get` | A change to that document. |\n| Full table query | A change in that table. |\n| Indexed query | A change whose old or new index position intersects the relevant range. |\n\nBounded indexed reads track the part of the range that can affect their result. Ignotum may refresh\na query whose returned value ends up unchanged. It favors an unnecessary rerun over missing a\nrelevant write.\n\nThe mutation promise does not wait for every affected query to render its next value. The commit\nis complete when the promise resolves, while query refresh and UI rendering continue separately.\n\nAfter a temporary disconnect, the browser keeps the latest settled query result. It recreates the\nsubscription after reconnecting. Ignotum checks recovered hosted query state against current app\nstate before serving it, so a commit that happened during recovery cannot make an older cached\nresult look current.\n\nThere is no delivery-time guarantee. A broken connection or unavailable runtime can delay an\nupdate, and infrastructure failures may reach the UI error boundary. Realtime queries are current\nresults, not an event log. Intermediate values may be coalesced when several mutations happen\nbefore a refresh completes.\n\n## Results do not move backward\n\nEach query snapshot carries the revision it observed. The client ignores a snapshot older than the\nlatest revision already accepted for that subscription. A slow earlier refresh cannot replace a\nnewer result.\n\nThe revision is an ordering value for complete query results. It is not exposed as an application\nevent number, and the public client API does not provide a change stream or resume token.\n\n## Pagination is not a frozen snapshot\n\nOne page is consistent in the same way as any other query result. Several pages loaded at different\ntimes do not share one historical snapshot.\n\nThe cursor records the query and the last returned position. It prevents using a cursor with a\ndifferent table, index, range, or direction. It does not freeze rows or index positions. If data is\ninserted, deleted, or reordered between page loads, an item can move across the cursor boundary.\n\nThe client subscribes to every loaded page, so earlier pages can also change while the list is\nopen. Render with document IDs and treat the combined list as a live ordered view.\n\n## Deployment activation is all or nothing\n\nA deployment contains its client and server code together. Ignotum changes the active deployment\nonly after the complete upload passes validation and any new indexes are ready. A build, upload,\nvalidation, or compatibility failure leaves the current deployment active.\n\nActivation reloads connected clients. A session never intentionally combines the client from one\ndeployment with functions from another.\n\n## What is not guaranteed\n\nIgnotum does not promise:\n\n- an order for mutation calls started concurrently;\n- permanent mutation deduplication beyond the published retention limits;\n- delivery of every intermediate query value;\n- a time bound for realtime updates or reconnects to succeed;\n- snapshot isolation across several pagination calls;\n- uniqueness for an application index;\n- uninterrupted hosted availability during a platform or persistent-data outage.\n\n[Limits](limits.md) lists the bounds that qualify these guarantees.\n"],
|
|
34289
|
+
["how-ignotum-works.md", "---\ntitle: How Ignotum works\ndescription: Understand how data, functions, files, Web code, and deployments fit together.\nsection: Start\nslug: how-ignotum-works\norder: 3\n---\n\n# How Ignotum works\n\nAn Ignotum app is one project with a database, functions, application files, and a browser-facing\nWeb part. A deployment publishes all of them together.\n\n## Database and functions\n\nThe schema names the app's tables and describes every stored field. Queries read the database.\nMutations read and write it in atomic operations.\n\nEach exported query or mutation receives a generated reference. Web code uses that reference\ninstead of a handwritten route or request type. The function's argument, result, and application\nerror definitions determine the corresponding TypeScript types in the browser.\n\n## Realtime results\n\nCalling a query from Web code creates a subscription. Ignotum records the database reads made by\nthe query and refreshes it after a committed mutation may have changed those reads. The UI receives\nthe current complete result rather than a stream of database operations.\n\nSeveral components using the same query and arguments share one browser subscription. A temporary\ndisconnect keeps the latest settled result while the client reconnects and recreates active\nsubscriptions.\n\n## Files\n\nApplication files are values that can appear in mutation arguments and database documents. Web\ncode passes a browser `File` to a mutation. The mutation stores the validated file value in a\ndocument, and a query can return it to Web code with a URL for the current query result.\n\nFiles in the top-level `public` directory are different. They are fixed deployment assets with\nstable paths and do not belong to database documents.\n\n## Web\n\nThe Web part starts at `client/index.tsx`. Its default `app(...)` export supplies the page title and\nroot Preact component. It calls functions through generated references and renders their typed\nresults.\n\n## Deployment\n\n`ignotum deploy` publishes the Web code, functions, schema, and public assets as one deployment.\nActivation changes them together. Existing browser sessions reload when they detect the new\ndeployment.\n\nNext: [Project structure](project-structure.md)\n"],
|
|
34290
|
+
["index.md", "---\ntitle: Ignotum\nlabel: Introduction\ndescription: Learn the application model used by Ignotum.\nsection: Start\nslug: /\norder: 1\n---\n\n# Ignotum\n\n> Ignotum is currently in private alpha. APIs and hosted service behavior may change before public\n> availability.\n\nIgnotum is an application cloud for TypeScript apps. An app defines its data, callable functions,\nbrowser UI, and files in one project. The CLI runs the project locally and deploys it as one unit.\n\nThe database stores application data. Queries read that data, mutations change it, and generated\nreferences let the Web part call both with matching TypeScript types. Active queries receive new\nresults after relevant mutations commit.\n\nStart with the [Quickstart](quickstart.md). It creates a small app and follows the path from schema\nto deployment. [How Ignotum works](how-ignotum-works.md) explains how the parts fit together, and\n[Project structure](project-structure.md) explains the `client`, `server`, and `shared` directories.\n\nNext: [Quickstart](quickstart.md)\n"],
|
|
34291
|
+
["indexes.md", "---\ntitle: Indexes\ndescription: Define indexes and use them for ranges and ordering.\nsection: Database\nslug: indexes\norder: 15\n---\n\n# Indexes\n\nAn index gives documents an order that functions can select and bound. Declare each index with its\ntable in `server/schema.ts`:\n\n```ts\ntodos: table({\n channel: values.string(),\n priority: values.integer(),\n text: values.string(),\n}).index(\"by_channel_priority\", [\"channel\", \"priority\"]),\n```\n\nIndex fields must be required booleans, dates, IDs, integers, numbers, strings, or literals. Field\norder matters. `by_channel_priority` supports a range for one channel, or for one channel and its\npriorities. It is not a priority-only index.\n\nAn index name must be non-empty, cannot begin with `$`, and is limited to 64 UTF-8 bytes. A table\ncannot repeat a name, repeat a field inside one index, or declare two indexes with the same fields\nin the same order. See [Limits](limits.md) for index counts and key size.\n\n## Select an index\n\nUse the declared name in a function:\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\nThe range builder follows the declared field order. Match any number of leading fields with `eq`,\nthen optionally bound the next field:\n\n| Method | Bound |\n| ------------------- | ------------------------------------------------- |\n| `eq(field, value)` | Equal to `value`. May continue to the next field. |\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\nChain one lower and one upper bound on the same field for a two-sided range:\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 reads the whole index in its declared order:\n\n```ts\nconst byPriority = yield * ctx.db.query(\"todos\").index(\"by_priority\").collect();\n```\n\nEvery index orders equal declared values by `createdAt`, then `id`. After matching all declared\nfields with `eq`, those fields are also available to the range builder.\n\n`order(\"desc\")` reverses the complete index order. It cannot sort by a field outside the selected\nindex.\n\nUse [Pagination](pagination.md) for large or incremental index results.\n"],
|
|
34292
|
+
["limits.md", "---\ntitle: Limits\ndescription: Understand hosted limits for functions, data, connections, and deployments.\nsection: Reference\nslug: limits\norder: 71\n---\n\n# 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 sync admission, execution time, result size, and app storage quota failures as\n`ResourceLimitExceeded`. Some lower-level document, collection, memory, stack, or operation limit\nfailures become `InternalServerError` instead. A deployment that exceeds a limit fails before\nactivation, so the current deployment stays active.\n\n## 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 a query when app data changes while it runs. One logical 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\nThe inventory contains at most 512 listed files. Its own `inventory.json` upload is additional.\nRequests with JSON bodies to `api.ignotum.cloud` are also limited to 1 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"],
|
|
34293
|
+
["local-development.md", "---\ntitle: Local development\ndescription: Run an Ignotum app locally and manage its local data.\nsection: Development\nslug: local-development\norder: 60\n---\n\n# Local development\n\nRun the development server from the app directory:\n\n```sh\nnpx ignotum dev\n```\n\nThe command requires `client/index.tsx` and `server/schema.ts`. It updates generated code, starts\nthe Web and function runtimes, and prints the local URL. The default is\n<http://127.0.0.1:3210>.\n\nClient changes use hot reload. Changes to server `.ts` files reload the function modules and\nrefresh active queries. Adding or removing a function module also updates the API references.\n\nThe development server uses one local database for the app directory. A second server for the same\napp fails with a message that identifies the process holding the database lock.\n\n## Options\n\nUse flags to change the listener or open a browser:\n\n```sh\nnpx ignotum dev --host 0.0.0.0 --port 3000 --open\n```\n\n| Flag | Default | Meaning |\n| -------- | ----------- | ------------------------------------------------------------------ |\n| `--host` | `127.0.0.1` | Address used by the local server. |\n| `--port` | `3210` | TCP port from 1 through 65,535. The command fails if it is in use. |\n| `--open` | off | Open the app in the default browser after startup. |\n\n## Local data\n\nLocal data survives restarts and lives under `.ignotum/dev`. The generated project ignores\n`.ignotum` in Git.\n\nStop the development server before resetting the database:\n\n```sh\nnpx ignotum dev db reset\n```\n\nThe command refuses to reset data while a live development server holds the lock. A successful\nreset removes the local database. The next start creates an empty database from the current schema.\n\nLocal development does not reproduce every hosted resource limit or the hosted function runtime.\nRun the typecheck and review [Limits](limits.md) before deployment.\n\nNext: [Generated code](generated-code.md)\n"],
|
|
34294
|
+
["mutations.md", "---\ntitle: Mutations\ndescription: Define and call typed mutations.\nsection: Functions\nslug: mutations\norder: 23\n---\n\n# Mutations\n\nA mutation reads and changes application data in one atomic operation. Web code calls it through a\ngenerated reference and receives a typed value or application error.\n\n## Define a mutation\n\nImport `mutation` and schema-bound `values` from `_generated/server.ts`:\n\n```ts\nimport { mutation, values } from \"@/_generated/server.js\";\n\nexport const create = mutation({\n args: { text: values.string() },\n returns: values.id(\"todos\"),\n\n handler: function* (ctx, { text }) {\n return yield* ctx.db.insert(\"todos\", {\n text,\n completed: false,\n });\n },\n});\n```\n\nMutation `ctx.db` includes all read methods plus `insert`, `patch`, `replace`, and `delete`. See\n[Writing data](writing-data.md) for their exact behavior.\n\nAll reads and writes in one handler belong to one transaction. A successful return commits them\ntogether. An application error, thrown exception, invalid return, limit failure, or platform\nfailure commits none of them. Other mutations for the same app do not interleave with the handler.\n\n## Call a mutation from Web code\n\n`useMutation` takes the generated reference and returns a stable typed function:\n\n```tsx\nimport { Result, useMutation } from \"ignotum/client\";\nimport { api } from \"@/_generated/api.js\";\n\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 promise resolves with a settled `Result` after the mutation commits or returns a declared\napplication error. It rejects for client or protocol failures, including unsupported argument\nvalues, an unavailable function, or a hosted limit that prevents the call from starting.\n\n## Handle the result\n\nThe client `Result` export contains `match`. Server operations such as `Result.fail`,\n`Result.succeed`, `Result.try`, `yield*`, and `.catch()` are not available in browser code.\n\nApplication errors use the `error` handler. A single function receives their union. An object must\nhandle every `_tag` separately. The optional `internalError` handler receives an internal failure's\nrequest ID. Without that handler, `Result.match` throws the error so an error boundary can handle\nit. See [Errors](errors.md).\n\n## Reconnects and retries\n\nThe client assigns one invocation ID to a mutation call. If the connection drops before the result\narrives, it keeps the promise pending and sends the same invocation after a matching reconnect.\nIgnotum returns a retained result instead of applying the mutation twice.\n\nA deployment change reloads the page and does not replay pending work against different function\ncode. Retry records have retention limits and are not permanent duplicate protection for business\noperations. See [Guarantees](guarantees.md) and [Limits](limits.md).\n\nSuccessful mutations can refresh active queries after the commit. The mutation result can reach\nits caller before every affected query renders its next value.\n\nNext: [Files](files.md)\n"],
|
|
34295
|
+
["pagination.md", "---\ntitle: Pagination\ndescription: Return cursor-based pages and load them from Web code.\nsection: Database\nslug: pagination\norder: 16\n---\n\n# Pagination\n\nA paginated query accepts `values.pagination()` and returns `values.page(...)`:\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\nThe result has `items` and `nextCursor`. A `null` cursor means the query was exhausted. Pass a\nnon-null cursor back with the same table, index, range, and order. A cursor from another query is\nrejected. Treat cursors as opaque strings.\n\nPage sizes are integers from 1 through 1,000. A page returns no more than the requested size.\n\n## Load pages in Web code\n\n`usePaginatedQuery` supplies the cursor and page size. Callers pass only the function's other\narguments:\n\n```tsx\nimport { Result, usePaginatedQuery } from \"ignotum/client\";\n\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\nThe default page size is 20. Pass an options object as the third argument to use a value from 1\nthrough 1,000:\n\n```tsx\nconst events = usePaginatedQuery(api.events.list, { project: \"api\" }, { pageSize: 50 });\n```\n\nIf pagination is the query's only argument, omit the second argument. `Query.skip` is accepted in\nplace of arguments and keeps the result pending.\n\nThe successful value has these fields:\n\n| Field | Meaning |\n| ------------ | --------------------------------------------- |\n| `items` | Items from every loaded page, in query order. |\n| `loadMore()` | Requests one more page. |\n| `status` | `CanLoadMore`, `LoadingMore`, or `Exhausted`. |\n\nEarlier items remain visible while another page loads. Changing the query arguments or page size\nstarts again at the first page.\n\n## Live page behavior\n\nPages do not freeze the database at the first page's revision. Each call reads current committed\nstate and continues after its cursor position. Data that moves across that position between calls\ncan appear on a different page or no longer appear in the remaining pages.\n\nEach loaded page remains subscribed. Use item IDs when rendering and allow items to move in a\nchanging list. See [Guarantees](guarantees.md) for the complete pagination contract.\n"],
|
|
34296
|
+
["preact.md", "---\ntitle: Preact\ndescription: Use the Preact components, hooks, events, and types exposed by Ignotum.\nsection: Web\nslug: preact\norder: 42\n---\n\n# Preact\n\n`ignotum/client` supplies Preact's JSX runtime. Import components, hooks, and types from this entry\npoint instead of adding a second UI runtime.\n\nThe main exports are:\n\n| Area | Exports |\n| ----------- | --------------------------------------------------------------------------------------- |\n| Elements | `Fragment`, `createElement`, `h`, `cloneElement`, `createContext`, `createRef` |\n| Components | `Component`, `isValidElement`, `toChildArray` |\n| Hooks | `useState`, `useEffect`, `useMemo`, `useCallback`, `useReducer`, `useRef`, `useContext` |\n| Other hooks | `useId`, `useLayoutEffect`, `useImperativeHandle`, `useDebugValue`, `useErrorBoundary` |\n\nThe entry point also exports Preact's component, event, JSX, ref, and hook types. TypeScript usually\ninfers them from JSX and hook calls.\n\nUse the JSX `class` attribute. Event handlers receive Preact's typed DOM events:\n\n```tsx\nimport { useState, type TargetedInputEvent } from \"ignotum/client\";\n\nfunction Search() {\n const [text, setText] = useState(\"\");\n\n const update = (event: TargetedInputEvent<HTMLInputElement>) => {\n setText(event.currentTarget.value);\n };\n\n return <input class=\"rounded border px-3 py-2\" value={text} onInput={update} />;\n}\n```\n\nIgnotum-specific browser exports include `app`, `Files`, `Query`, `Result`, `useQuery`,\n`usePaginatedQuery`, and `useMutation`. Their complete workflows are documented in\n[Files](files.md), [Queries](queries.md), and [Mutations](mutations.md).\n\nNext: [Styling](styling.md)\n"],
|
|
34297
|
+
["project-structure.md", "---\ntitle: Project structure\ndescription: Place Web code, functions, data definitions, and shared code in an Ignotum app.\nsection: Start\nslug: project-structure\norder: 4\n---\n\n# Project structure\n\nAn Ignotum project separates browser code from function code while keeping both in one app:\n\n```text\nclient/\n index.tsx\nserver/\n schema.ts\n todos.ts\nshared/\n_generated/\npublic/\n```\n\nOnly `client/index.tsx` and `server/schema.ts` are required. The other paths appear when the app\nneeds them.\n\n## `client`\n\n`client` contains the Web part. Its `index.tsx` file default-exports one `app(...)` definition:\n\n```tsx\nimport { app } from \"ignotum/client\";\n\nfunction App() {\n return <main>My app</main>;\n}\n\nexport default app({\n title: \"My app\",\n component: App,\n});\n```\n\nClient modules import browser APIs and UI exports from `ignotum/client`. They may also import\n`_generated/api.ts`, `_generated/types.ts`, other client modules, shared modules, and\nbrowser-compatible dependencies. They cannot import server modules or `ignotum/server`.\n\n## `server`\n\n`server/schema.ts` defines the database schema. Other `.ts` files directly inside `server` may\nexport queries and mutations. Their file names become generated API modules. For example,\n`server/todos.ts` produces references such as `api.todos.list`.\n\nSchema definitions import `defineSchema` from `ignotum/server`. Functions import schema-bound\n`query`, `mutation`, and `values` from `@/_generated/server.js`. Server modules may import other\nserver modules, shared modules, generated server and data types, and supported dependencies. They\ncannot import client modules or `ignotum/client`.\n\n## `shared`\n\n`shared` contains code that is valid in both runtimes. Shared modules cannot depend on browser-only\nor function-only APIs. Put a type, validator-independent helper, or constant here only when both\nsides use it.\n\n## `_generated`\n\nIgnotum writes generated function references, schema-bound builders, and data types to\n`_generated`. Do not edit these files. See [Generated code](generated-code.md).\n\n## `public`\n\nThe optional top-level `public` directory contains fixed files that keep their relative paths as\nWeb URLs. It is separate from application files stored in database documents. See\n[Public assets](public-assets.md).\n\nNext: [Schema](schema.md)\n"],
|
|
34298
|
+
["public-assets.md", "---\ntitle: Public assets\ndescription: Add a favicon and fixed files to the Web part.\nsection: Web\nslug: public-assets\norder: 44\n---\n\n# Public assets\n\nPublic assets belong to a deployment and keep stable URL paths. They are different from\n[application files](files.md), which belong to database documents and require a current query\nresult.\n\n## Favicon\n\nAdd `client/icon.svg` to use an SVG favicon. Ignotum discovers this exact path. Without the file,\nthe page has no favicon link.\n\nThe icon must be a regular file. Other SVG files cannot go in `public`, but imported SVG assets may\nstill be part of client code.\n\n## `public`\n\nPut files that need fixed URLs in a top-level `public` directory next to `client` and `server`:\n\n```text\npublic/\n images/\n logo.png\n manual.pdf\n```\n\nThese files become `/images/logo.png` and `/manual.pdf`. `client/public` is not supported.\n\nIgnotum accepts AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF. It checks the file signature against the\nextension. Entries must be regular files or directories, not symbolic links. Each path segment may\ncontain letters, numbers, `.`, `_`, `~`, or `-`. The first segment cannot be `_ignotum`, which is\nreserved for platform paths.\n\nEach file is limited to 16 MiB, and all public files count toward the deployment limits. See\n[Limits](limits.md).\n"],
|
|
34299
|
+
["queries.md", "---\ntitle: Queries\ndescription: Define, call, and subscribe to typed queries.\nsection: Functions\nslug: queries\norder: 22\n---\n\n# Queries\n\nA query reads application data and returns a typed result. Web code calls it through a generated\nreference and stays subscribed to relevant data changes.\n\n## Define a query\n\nImport `query` and schema-bound `values` from `_generated/server.ts`. Define the arguments, return\nvalue, and handler together:\n\n```ts\nimport { query, values } from \"@/_generated/server.js\";\n\nexport const get = query({\n args: { id: values.id(\"todos\") },\n returns: values.doc(\"todos\"),\n\n handler: function* (ctx, { id }) {\n return yield* ctx.db.get(\"todos\", id);\n },\n});\n```\n\nQueries have a read-only `ctx.db`. Use [Reading data](reading-data.md) for document and table reads,\nand [Indexes](indexes.md) for filtered and ordered results.\n\n## Call a query from Web code\n\nThe exported function above becomes `api.todos.get`. Pass that reference to `useQuery`:\n\n```tsx\nimport { Query, Result, useQuery } from \"ignotum/client\";\nimport { api } from \"@/_generated/api.js\";\nimport type { Id } from \"@/_generated/types.js\";\n\nfunction Todo({ id }: { id: Id<\"todos\"> | undefined }) {\n const todo = useQuery(api.todos.get, id === undefined ? Query.skip : { id });\n\n return Result.match(todo, {\n pending: () => <p>Loading...</p>,\n value: (value) => <p>{value.text}</p>,\n error: {\n DocumentNotFound: () => <p>The todo no longer exists.</p>,\n },\n });\n}\n```\n\nArgument-free queries take only the reference:\n\n```tsx\nconst todos = useQuery(api.todos.list);\n```\n\nQueries with arguments require the second parameter. The generated reference supplies its type.\n`Query.skip` keeps the hook pending and opens no subscription. Changing from `Query.skip` to\narguments starts the query. Changing the arguments switches the subscription and returns pending\nuntil the new query has a result.\n\n## Handle the result\n\n`useQuery` first returns a pending `QueryResult`, then a value or application error.\n`Result.match` requires handlers for the states and declared errors in its type:\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\nUse one `error` function when the UI treats all application errors alike. Use an object keyed by\n`_tag` when errors need different handling. `internalError` is optional. Without it, an\n`InternalServerError` is thrown to the nearest UI error boundary. See [Errors](errors.md).\n\n## Realtime behavior\n\nThe hook keeps its subscription until the component stops using it or its reference and arguments\nchange. Components using the same query and arguments share one browser subscription.\n\nAfter a successful mutation, Ignotum refreshes active queries whose recorded reads may have\nchanged. A document lookup responds to writes for that document. An indexed range responds to\nchanges inside its relevant range. A full table query responds to writes in that table.\n\nA lost connection does not change a settled query back to pending. The client keeps the latest\nresult, reconnects with an increasing delay capped at five seconds, then recreates active\nsubscriptions. A deployment change reloads the page.\n\nUse [Pagination](pagination.md) for paginated query definitions and `usePaginatedQuery`. Read\n[Guarantees](guarantees.md) for consistency, reconnect, and update behavior.\n\nNext: [Mutations](mutations.md)\n"],
|
|
34300
|
+
["quickstart.md", "---\ntitle: Quickstart\ndescription: Create, understand, and deploy a small Ignotum app.\nsection: Start\nslug: quickstart\norder: 2\n---\n\n# Quickstart\n\nIgnotum requires Node.js 22.18 or newer. Create the generated counter app and start it:\n\n```sh\nnpx ignotum new my-app\ncd my-app\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. The page shows a counter backed by an Ignotum database.\n\n## Find the app parts\n\nThe files used in this guide are:\n\n```text\nclient/\n index.tsx\nserver/\n counter.ts\n schema.ts\nshared/\n utils.ts\n_generated/\n```\n\n`server/schema.ts` defines stored data. `server/counter.ts` defines the query and mutation.\n`client/index.tsx` defines the Web app and calls those functions. `shared/utils.ts` is available to\nboth runtimes. Ignotum owns the files under `_generated`.\n\n## Define data\n\nThe generated schema has one table:\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\nThe schema supplies runtime validation and the document types used by functions and Web code.\n\n## Define a query\n\n`server/counter.ts` exports `get`:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.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```\n\nCode generation gives this function the reference `api.counter.get`. The Web app subscribes to it\nwith `useQuery`:\n\n```tsx\nconst count = useQuery(api.counter.get);\n```\n\n`Result.match` renders the pending state and the returned value:\n\n```tsx\n{\n Result.match(count, {\n pending: () => <p>Loading...</p>,\n value: (value) => <p>{value}</p>,\n });\n}\n```\n\n## Define a mutation\n\nThe generated `increment` mutation either inserts the first counter document or updates the\nexisting one:\n\n```ts\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) + 1;\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\nWeb code obtains a typed caller from the generated reference:\n\n```tsx\nconst increment = useMutation(api.counter.increment);\n\n<button type=\"button\" onClick={() => void increment()}>\n Increment\n</button>;\n```\n\nClick the button in one browser window. The mutation commits the new value, then the active query\nreceives an updated result. Open a second window to see both subscriptions follow the same data.\n\n## Deploy the app\n\nSet the API token you were issued, then choose a globally available app slug:\n\n```sh\nexport IGNOTUM_API_TOKEN=your-token\nnpx ignotum deploy --app my-app\n```\n\nThe command prints the hosted URL and links this directory to the hosted app. Later deployments use\nthe saved link:\n\n```sh\nnpx ignotum deploy\n```\n\nRead [Deploy an app](deploy-app.md) for token handling, slug rules, and deployment failure behavior.\n\nNext: [How Ignotum works](how-ignotum-works.md)\n"],
|
|
34301
|
+
["reading-data.md", "---\ntitle: Reading data\ndescription: Read individual documents and table results.\nsection: Database\nslug: reading-data\norder: 13\n---\n\n# Reading data\n\nQuery and mutation handlers read documents through `ctx.db`. A read can name one document, scan a\ntable, or select a declared index. Ignotum does not turn an unindexed filter into a hidden table\nscan. Use an [index](indexes.md) for a filtered database read, or read the table and filter it when\nthe table is intentionally small.\n\n## One document\n\nUse `find` when absence is expected:\n\n```ts\nconst todo = yield * ctx.db.find(\"todos\", args.id);\n// Doc<\"todos\"> | undefined\n```\n\nUse `get` when absence is an application error path:\n\n```ts\nconst todo = yield * ctx.db.get(\"todos\", args.id);\n// Doc<\"todos\">\n```\n\nA missing `get` fails with `DocumentNotFound`. The error contains `table` and `id`. Catch it to\nrecover or map it to a public error. If it remains in the handler, it becomes part of that\nfunction's typed application errors.\n\nIDs are scoped to their tables at compile time. A `users` ID cannot be passed to a `todos` read.\n\n## A table\n\nStart a table query with `ctx.db.query`:\n\n```ts\nconst todos = yield * ctx.db.query(\"todos\").collect();\n```\n\nA table query is ordered by `createdAt`, then `id`, both ascending. Reverse both fields with\n`order(\"desc\")`:\n\n```ts\nconst newest = yield * ctx.db.query(\"todos\").order(\"desc\").take(20);\n```\n\nUse a table query when the function needs a table-wide result. Any write in that table can cause a\nsubscribed query that used the result to refresh.\n\n## Result methods\n\nFinish a table or index query with one method:\n\n| Method | Result |\n| ------------------- | ------------------------------------------------------------------------------ |\n| `collect()` | Every matching document, within hosted collection limits. |\n| `take(count)` | At most `count` documents. `count` may be 0 through 1,000. |\n| `first()` | The first document, or `undefined`. |\n| `unique()` | The only document, or `undefined`. More than one match is an internal failure. |\n| `paginate(options)` | One page and an opaque cursor. |\n\n`unique()` checks a result. It does not make the index unique and does not prevent duplicate field\nvalues. Use `first()` when multiple matches are valid.\n\nPrefer `take` or `first` when the function only needs a bounded result. `collect` is capped at\n1,000 documents when hosted and may hit byte limits first.\n\n## Reads inside a mutation\n\nA mutation sees writes it made earlier in the same handler, including through table and index\nqueries:\n\n```ts\nconst todo = yield * ctx.db.get(\"todos\", args.id);\nyield * ctx.db.patch(\"todos\", todo.id, { completed: !todo.completed });\nconst changed = yield * ctx.db.get(\"todos\", todo.id);\n```\n\nNo other mutation can interleave with those operations for the same app. See\n[Writing data](writing-data.md) and [Guarantees](guarantees.md).\n"],
|
|
34302
|
+
["schema.md", "---\ntitle: Schema\ndescription: Define tables, fields, IDs, and stored document types.\nsection: Database\nslug: schema\norder: 11\n---\n\n# Schema\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`createdAt` is set when Ignotum inserts the document. `updatedAt` starts at the same time and\nadvances on every successful patch or replacement. Both are JavaScript `Date` values in function\nand Web code.\n\nChain `index` after a table to declare an ordered index. [Indexes](indexes.md) covers field rules,\nranges, and ordering.\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\n## Schema changes\n\nLocal development updates tables and indexes as the schema changes. Existing documents still have\nto match the current field validators when a function reads them.\n\nFor hosted apps, the first active deployment fixes table names, field names, and field validators.\nLater deployments may add, remove, rename, or reorder indexes. They may also change functions and\nWeb code. A deployment that changes the stored table or field schema is built and uploaded, but\nactivation fails with `SchemaIncompatible`, leaving the current deployment active.\n\nSchema migrations are not part of the current public API. Plan the first hosted schema with this\nrestriction in mind.\n\nNext: [Queries](queries.md)\n"],
|
|
34303
|
+
["styling.md", "---\ntitle: Styling\ndescription: Style Web code with Tailwind CSS or imported CSS files.\nsection: Web\nslug: styling\norder: 43\n---\n\n# Styling\n\nIgnotum loads Tailwind CSS automatically. An app does not need a Tailwind configuration file or a\nframework stylesheet.\n\nUse utility classes through JSX's `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\nCustom CSS is an ordinary client module. Give the file any name and import it from client code:\n\n```tsx\nimport \"./calendar.css\";\n```\n\nNext: [Public assets](public-assets.md)\n"],
|
|
34304
|
+
["values.md", "---\ntitle: Values\ndescription: Use runtime validators and their matching TypeScript types.\nsection: Database\nslug: values\norder: 12\n---\n\n# Values\n\nUse `values` validators to describe table fields, function arguments, return values, and application\nerrors. The same definition supplies a TypeScript type and a runtime check. Do not substitute a\nTypeScript-only interface where Ignotum asks for a validator.\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.file(options?)` | `FileValue` | Accepts configured image formats and a byte limit. See [Files](files.md). |\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\nDates cross the Web and function boundary as `Date` instances. Ignotum rejects invalid dates.\n`values.number()` rejects `NaN`, `Infinity`, and `-Infinity`. Use `values.integer()` when fractional\nvalues should also be rejected.\n\n`values.file()` accepts JPEG, PNG, WebP, AVIF, and GIF by default. Pass `formats` to allow a\nnon-empty subset and `maxBytes` to set a positive limit up to 10 MiB. [Files](files.md) covers the\ncomplete file workflow and the places where file values are allowed.\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 returns a new validator and leaves the source unchanged. The next method works from the\ncurrent result. Added fields can be picked, omitted, or overridden immediately. An omitted name can\nbe 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\nFixed object validators describe named fields. Use `values.record(value)` when keys are not known\nin advance.\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\nWrapping a validator in both supports omission and an explicit `null`:\n\n```ts\nvalues.optional(values.nullable(values.date()));\n```\n"],
|
|
34305
|
+
["web.md", "---\ntitle: Web overview\nlabel: Overview\ndescription: Define the browser-facing part of an Ignotum app.\nsection: Web\nslug: web\norder: 40\n---\n\n# Web overview\n\nThe Web part of an Ignotum app lives in `client`. It renders the browser UI and calls queries and\nmutations through `_generated/api.ts`.\n\n`client/index.tsx` is the entry. It default-exports an `app(...)` definition with the page title and\nroot Preact component. Ignotum supplies the JSX runtime and Tailwind CSS.\n\n- [App entry](app-entry.md) documents `client/index.tsx` and the `app(...)` definition.\n- [Preact](preact.md) lists the UI exports and import rules.\n- [Styling](styling.md) covers Tailwind CSS and custom CSS.\n- [Public assets](public-assets.md) covers the favicon and files with fixed URL paths.\n- [Queries](queries.md) and [Mutations](mutations.md) cover the generated API and browser hooks.\n\nClient modules may import `_generated/api.ts`, `_generated/types.ts`, other client modules, shared\nmodules, and browser-compatible dependencies. They cannot import files under `server`,\n`_generated/server.ts`, `ignotum/server`, Node built-ins, or native add-ons.\n\nNext: [App entry](app-entry.md)\n"],
|
|
34306
|
+
["writing-data.md", "---\ntitle: Writing data\ndescription: Insert, patch, replace, and delete documents in a mutation.\nsection: Database\nslug: writing-data\norder: 14\n---\n\n# Writing data\n\nOnly mutation handlers can write. Their `ctx.db` has four write methods and all methods from\n[Reading data](reading-data.md).\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 application field and omit optional fields that have no value. Ignotum sets\n`id`, `createdAt`, and `updatedAt`. Those system fields are not valid insert input.\n\n## Patch\n\n`patch` changes the supplied fields and leaves the rest alone:\n\n```ts\nyield * ctx.db.patch(\"todos\", args.id, { completed: true });\n```\n\nThe patch input is type-checked against the table, and every field is optional. A matching\ndocument keeps its `id` and `createdAt`; its `updatedAt` advances.\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. A matching document keeps its `id` and `createdAt`; its\n`updatedAt` advances.\n\n## Delete\n\n`delete` removes a document by ID:\n\n```ts\nyield * ctx.db.delete(\"todos\", args.id);\n```\n\n## Missing targets\n\n`patch`, `replace`, and `delete` fail with `DocumentNotFound` when the target document does not\nexist. The error contains the target `table` and `id`. Catch it directly or map it to a public\napplication error as described in [Errors](errors.md).\n\nUse `find` first when a missing target should be ignored:\n\n```ts\nconst todo = yield * ctx.db.find(\"todos\", args.id);\nif (todo !== undefined) yield * ctx.db.delete(\"todos\", args.id);\n```\n\n## Transaction behavior\n\nA mutation reads one logical state, sees its own earlier writes, and commits all successful writes\ntogether. Other mutations for the same app do not interleave with it.\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\nThe commit happens only after the handler returns a valid success value. A declared application\nerror, thrown exception, invalid return, limit failure, or platform failure commits none of the\nmutation's writes.\n\nIgnotum validates stored fields and maintains declared indexes as part of the same atomic write.\nIf the mutation would exceed the hosted app storage quota, the whole mutation rolls back.\n\nSuccessful mutations cause active queries to refresh when their recorded reads may have changed.\nThe mutation result can reach its caller before every affected query has delivered its next\nsnapshot, so do not treat subscription rendering as part of the mutation promise. See\n[Guarantees](guarantees.md).\n"]
|
|
34307
|
+
];
|
|
34308
|
+
const withoutFrontmatter = (content) => content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
|
|
32994
34309
|
const agentAppFiles = [
|
|
32995
34310
|
{
|
|
32996
|
-
content:
|
|
34311
|
+
content: AGENTS_default,
|
|
32997
34312
|
path: "AGENTS.md"
|
|
32998
34313
|
},
|
|
32999
34314
|
{
|
|
33000
|
-
content:
|
|
34315
|
+
content: SKILL_default,
|
|
33001
34316
|
path: ".agents/skills/ignotum/SKILL.md"
|
|
33002
34317
|
},
|
|
33003
|
-
...[
|
|
33004
|
-
|
|
33005
|
-
["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"],
|
|
33006
|
-
["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"],
|
|
33007
|
-
["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"],
|
|
33008
|
-
["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 access, and errors.\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"],
|
|
33009
|
-
["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 mutations safely.\nDeployment files do not count as stored app data.\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"],
|
|
33010
|
-
["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"],
|
|
33011
|
-
["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 }),\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## 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"],
|
|
33012
|
-
["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 reads\n\nUse `find` when a missing document is a normal 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 a typed `DocumentNotFound` value containing `table` and `id`. Collect a\nwhole table through a query:\n\n```ts\nconst todos = yield * ctx.db.query(\"todos\").collect();\n```\n\nQuery handlers only receive read methods.\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## Database writes\n\nMutation handlers receive the read methods and these writes:\n\n```ts\nconst id =\n yield *\n ctx.db.insert(\"todos\", {\n text: \"Learn Ignotum\",\n completed: false,\n });\n\nyield * ctx.db.patch(\"todos\", id, { completed: true });\n\nyield *\n ctx.db.replace(\"todos\", id, {\n text: \"Build an app\",\n completed: false,\n });\n\nyield * ctx.db.delete(\"todos\", id);\n```\n\n`patch` changes only supplied fields. `replace` requires every non-optional table field. Ignotum\nrolls back a mutation when its handler fails with a typed application error or encounters an\ninternal failure.\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"],
|
|
33013
|
-
["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.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` is available on the schema-bound `values` exported by `_generated/server.ts`. It is\nnot available while defining `server/schema.ts`, because the table definitions are still being\ncreated there.\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"]
|
|
33014
|
-
].map(([name, content]) => ({
|
|
33015
|
-
content,
|
|
34318
|
+
...references.map(([name, content]) => ({
|
|
34319
|
+
content: withoutFrontmatter(content),
|
|
33016
34320
|
path: `.agents/skills/ignotum/references/${name}`
|
|
33017
34321
|
}))
|
|
33018
34322
|
];
|
|
@@ -33203,6 +34507,17 @@ Open <http://127.0.0.1:3210>.
|
|
|
33203
34507
|
The app generator creates \`_generated\`. The dev server checks those files before it starts
|
|
33204
34508
|
and updates them when the schema or server functions change.
|
|
33205
34509
|
|
|
34510
|
+
## Deploy
|
|
34511
|
+
|
|
34512
|
+
Set \`IGNOTUM_API_TOKEN\`, then choose a unique slug on the first deployment:
|
|
34513
|
+
|
|
34514
|
+
\`\`\`sh
|
|
34515
|
+
npx ignotum deploy --app my-app
|
|
34516
|
+
\`\`\`
|
|
34517
|
+
|
|
34518
|
+
The command prints the hosted URL and records the app link in \`.ignotum/app.json\`. Later
|
|
34519
|
+
deployments use \`npx ignotum deploy\` without the \`--app\` flag.
|
|
34520
|
+
|
|
33206
34521
|
## App files
|
|
33207
34522
|
|
|
33208
34523
|
- \`server/schema.ts\` defines the database tables.
|
|
@@ -33212,6 +34527,7 @@ and updates them when the schema or server functions change.
|
|
|
33212
34527
|
- Ignotum loads Tailwind CSS automatically. Custom CSS files are ordinary client modules.
|
|
33213
34528
|
- \`shared/utils.ts\` contains code shared across the app.
|
|
33214
34529
|
- \`_generated\` contains Ignotum's generated types and bindings. Do not edit it by hand.
|
|
34530
|
+
- \`.agents/skills/ignotum\` contains the app skill and references used by coding agents.
|
|
33215
34531
|
|
|
33216
34532
|
Run the typechecker after a change:
|
|
33217
34533
|
|
|
@@ -33220,6 +34536,9 @@ npx ignotum codegen
|
|
|
33220
34536
|
npx tsc --noEmit
|
|
33221
34537
|
\`\`\`
|
|
33222
34538
|
|
|
34539
|
+
The [Ignotum documentation](https://docs.ignotum.cloud) covers schema values, indexes, queries,
|
|
34540
|
+
mutations, client hooks, deployment, guarantees, and hosted limits.
|
|
34541
|
+
|
|
33223
34542
|
## Claude Code
|
|
33224
34543
|
|
|
33225
34544
|
If you use Claude Code, rename \`AGENTS.md\` to \`CLAUDE.md\` and \`.agents\` to \`.claude\` so it
|