ignotum 0.0.7 → 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 +869 -175
- 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-Dp4J-xt-.d.ts → api-D9lV-5av.d.ts} +7 -9
- package/dist/runtime/client.d.ts +12 -5
- package/dist/runtime/client.js +275 -52
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/{descriptor-t6BOEGw9-C1rwYIlx.js → descriptor-C5VA9qRl-C338iC6l.js} +38 -7
- 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/{id-Btwac71X-DhnKYsjY.d.ts → id-Btwac71X-DGj0DQuu.d.ts} +50 -4
- package/dist/runtime/{index-B5KSOjGN.d.ts → index-CF04_Dps.d.ts} +39 -20
- package/dist/runtime/internal/api.d.ts +2 -2
- package/dist/runtime/internal/api.js +1 -1
- package/dist/runtime/internal/host.d.ts +8 -7
- package/dist/runtime/internal/host.js +21 -10
- 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-B1BzNkh8-BUSTbeSg.d.ts → pagination-DrOowBve-Bnipd34u.d.ts} +8 -4
- package/dist/runtime/{schema-D9RmboaS.js → schema-DJfwfq87.js} +17 -3
- 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 +4 -4
- package/src/cli/agent-files.ts +52 -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 +2 -15
- package/src/client/index.ts +7 -0
- package/src/client/sync.ts +137 -21
- package/src/dev-runtime/database.ts +69 -57
- package/src/dev-runtime/files.ts +338 -0
- package/src/dev-runtime/functions.ts +14 -6
- package/src/dev-runtime/migrations.ts +14 -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-C1rwYIlx.js.map +0 -1
- package/dist/runtime/id-D570vudg.js +0 -26
- package/dist/runtime/id-D570vudg.js.map +0 -1
- package/dist/runtime/pagination-BKPko9Hm.d.ts +0 -1
- package/dist/runtime/schema-D9RmboaS.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,
|
|
@@ -29654,6 +29809,49 @@ const getValueDescriptor = (value) => {
|
|
|
29654
29809
|
if (!Predicate.hasProperty(value, ValueDescriptorTypeId)) return void 0;
|
|
29655
29810
|
return Schema.is(ValueDescriptor)(value[ValueDescriptorTypeId]) ? value[ValueDescriptorTypeId] : void 0;
|
|
29656
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
|
+
};
|
|
29657
29855
|
//#endregion
|
|
29658
29856
|
//#region ../contracts/dist/deployment.js
|
|
29659
29857
|
const ArtifactPath = Schema.String.check(Schema.isPattern(/^(?!\/)(?![A-Za-z]:\/)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*(?:^|\/)\.(?:\/|$))(?!.*\/\/)[^\\\0]+$/)).pipe(Schema.brand("ignotum/deployment/ArtifactPath"));
|
|
@@ -29687,15 +29885,15 @@ const ClientRoute = Schema.Struct({
|
|
|
29687
29885
|
pathname: ClientPath,
|
|
29688
29886
|
artifact: ArtifactReference
|
|
29689
29887
|
});
|
|
29690
|
-
const DeploymentInventory = Schema.Struct({
|
|
29888
|
+
const DeploymentInventory = initial(Schema.Struct({
|
|
29691
29889
|
formatVersion: Schema.Literal(1),
|
|
29692
29890
|
files: Schema.Array(ArtifactFile)
|
|
29693
|
-
});
|
|
29694
|
-
const ClientManifest = Schema.Struct({
|
|
29891
|
+
}));
|
|
29892
|
+
const ClientManifest = initial(Schema.Struct({
|
|
29695
29893
|
formatVersion: Schema.Literal(1),
|
|
29696
29894
|
shell: ArtifactReference,
|
|
29697
29895
|
routes: Schema.Array(ClientRoute)
|
|
29698
|
-
});
|
|
29896
|
+
}));
|
|
29699
29897
|
const ClientRoutingRoute = Schema.Struct({
|
|
29700
29898
|
pathname: ClientPath,
|
|
29701
29899
|
artifact: ArtifactPath
|
|
@@ -29713,26 +29911,60 @@ const SchemaSnapshotIndex = Schema.Struct({
|
|
|
29713
29911
|
name: Schema.String,
|
|
29714
29912
|
fields: Schema.Array(Schema.String)
|
|
29715
29913
|
});
|
|
29914
|
+
const SchemaSnapshotTableV1 = Schema.Struct({
|
|
29915
|
+
name: Schema.String,
|
|
29916
|
+
fields: Schema.Array(SchemaSnapshotField)
|
|
29917
|
+
});
|
|
29918
|
+
const SchemaSnapshotV1 = Schema.Struct({
|
|
29919
|
+
formatVersion: Schema.Literal(1),
|
|
29920
|
+
tables: Schema.Array(SchemaSnapshotTableV1)
|
|
29921
|
+
});
|
|
29716
29922
|
const SchemaSnapshotTable = Schema.Struct({
|
|
29717
29923
|
name: Schema.String,
|
|
29718
29924
|
fields: Schema.Array(SchemaSnapshotField),
|
|
29719
29925
|
indexes: Schema.Array(SchemaSnapshotIndex)
|
|
29720
29926
|
});
|
|
29721
|
-
const
|
|
29927
|
+
const SchemaSnapshotV2 = Schema.Struct({
|
|
29722
29928
|
formatVersion: Schema.Literal(2),
|
|
29723
29929
|
tables: Schema.Array(SchemaSnapshotTable)
|
|
29724
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
|
+
});
|
|
29725
29944
|
const ServerFunctionArtifact = Schema.Struct({
|
|
29726
29945
|
address: FunctionAddress,
|
|
29727
29946
|
kind: FunctionKind,
|
|
29947
|
+
args: Schema.optional(ValueDescriptor),
|
|
29948
|
+
returns: Schema.optional(ValueDescriptor),
|
|
29949
|
+
errors: Schema.optional(ValueDescriptor),
|
|
29728
29950
|
bundle: ArtifactReference,
|
|
29729
29951
|
sourceMap: ArtifactReference
|
|
29730
29952
|
});
|
|
29731
|
-
const
|
|
29953
|
+
const ServerBuildManifestV1 = Schema.Struct({
|
|
29732
29954
|
formatVersion: Schema.Literal(1),
|
|
29733
29955
|
schema: ArtifactReference,
|
|
29956
|
+
functions: Schema.Array(ServerFunctionArtifactV1)
|
|
29957
|
+
});
|
|
29958
|
+
const ServerBuildManifestV2 = Schema.Struct({
|
|
29959
|
+
formatVersion: Schema.Literal(2),
|
|
29960
|
+
schema: ArtifactReference,
|
|
29734
29961
|
functions: Schema.Array(ServerFunctionArtifact)
|
|
29735
29962
|
});
|
|
29963
|
+
const ServerBuildManifest = upgrade(initial(ServerBuildManifestV1), ServerBuildManifestV2, (manifest) => ({
|
|
29964
|
+
formatVersion: 2,
|
|
29965
|
+
schema: manifest.schema,
|
|
29966
|
+
functions: manifest.functions
|
|
29967
|
+
}));
|
|
29736
29968
|
const deploymentInventoryPath = ArtifactPath.make("inventory.json");
|
|
29737
29969
|
const clientManifestPath = ArtifactPath.make("client/manifest.json");
|
|
29738
29970
|
const clientShellPath = ArtifactPath.make("client/shell.html");
|
|
@@ -29848,7 +30080,7 @@ var IdGenerator = class IdGenerator extends Context.Service()("@ignotum/shared/i
|
|
|
29848
30080
|
};
|
|
29849
30081
|
//#endregion
|
|
29850
30082
|
//#region package.json
|
|
29851
|
-
var version = "0.0.
|
|
30083
|
+
var version = "0.0.8";
|
|
29852
30084
|
//#endregion
|
|
29853
30085
|
//#region src/cli/codegen.ts
|
|
29854
30086
|
const generatedHeader = "// Generated by `ignotum codegen`. Do not edit.";
|
|
@@ -30202,11 +30434,11 @@ const IndexPosition = Schema.Struct({
|
|
|
30202
30434
|
key: Schema.String.check(Schema.isPattern(/^(?:[0-9a-f]{2})*$/)).pipe(Schema.brand("ignotum/runtime/EncodedIndexKey"))
|
|
30203
30435
|
});
|
|
30204
30436
|
const PaginationPosition = Schema.Union([TablePosition, IndexPosition]);
|
|
30205
|
-
const CursorPayload = Schema.Struct({
|
|
30437
|
+
const CursorPayload = initial(Schema.Struct({
|
|
30206
30438
|
version: Schema.Literal(1),
|
|
30207
30439
|
query: Schema.String,
|
|
30208
30440
|
position: PaginationPosition
|
|
30209
|
-
});
|
|
30441
|
+
}));
|
|
30210
30442
|
const CursorPayloadJson = Schema.fromJsonString(CursorPayload);
|
|
30211
30443
|
const paginationQueryIdentity = (tableId, index, lower, upper, order) => encodeCanonicalJson([
|
|
30212
30444
|
tableId,
|
|
@@ -30563,14 +30795,12 @@ Function$1.dual(2, (previous, next) => previous === void 0 || encodeCanonical(Sc
|
|
|
30563
30795
|
indexes: []
|
|
30564
30796
|
}))
|
|
30565
30797
|
}));
|
|
30566
|
-
|
|
30567
|
-
//#region src/cli/app-configuration.ts
|
|
30568
|
-
const AppConfiguration = Schema.Struct({
|
|
30798
|
+
const AppConfiguration = initial(Schema.Struct({
|
|
30569
30799
|
formatVersion: Schema.Literal(1),
|
|
30570
30800
|
appId: AppId,
|
|
30571
30801
|
slug: AppSlug,
|
|
30572
30802
|
apiUrl: Schema.URLFromString
|
|
30573
|
-
});
|
|
30803
|
+
}));
|
|
30574
30804
|
var AppConfigurationInvalid = class extends Schema.TaggedError()("AppConfigurationInvalid", {
|
|
30575
30805
|
cause: Schema.optional(Schema.Defect()),
|
|
30576
30806
|
message: Schema.String
|
|
@@ -31043,8 +31273,8 @@ var InvalidPublicFile = class extends Schema.TaggedError()("InvalidPublicFile",
|
|
|
31043
31273
|
path: Schema.String
|
|
31044
31274
|
}) {};
|
|
31045
31275
|
const pathSegmentPattern = /^[A-Za-z0-9._~-]+$/;
|
|
31046
|
-
const startsWith = (bytes, signature, offset = 0) => signature.every((byte, index) => bytes[offset + index] === byte);
|
|
31047
|
-
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);
|
|
31048
31278
|
const hasValidSignature = (extension, bytes) => {
|
|
31049
31279
|
switch (extension) {
|
|
31050
31280
|
case ".avif": {
|
|
@@ -31054,20 +31284,20 @@ const hasValidSignature = (extension, bytes) => {
|
|
|
31054
31284
|
return false;
|
|
31055
31285
|
}
|
|
31056
31286
|
case ".gif": return asciiAt(bytes, "GIF87a") || asciiAt(bytes, "GIF89a");
|
|
31057
|
-
case ".ico": return startsWith(bytes, [
|
|
31287
|
+
case ".ico": return startsWith$1(bytes, [
|
|
31058
31288
|
0,
|
|
31059
31289
|
0,
|
|
31060
31290
|
1,
|
|
31061
31291
|
0
|
|
31062
31292
|
]);
|
|
31063
31293
|
case ".jpeg":
|
|
31064
|
-
case ".jpg": return startsWith(bytes, [
|
|
31294
|
+
case ".jpg": return startsWith$1(bytes, [
|
|
31065
31295
|
255,
|
|
31066
31296
|
216,
|
|
31067
31297
|
255
|
|
31068
31298
|
]);
|
|
31069
31299
|
case ".pdf": return asciiAt(bytes, "%PDF-");
|
|
31070
|
-
case ".png": return startsWith(bytes, [
|
|
31300
|
+
case ".png": return startsWith$1(bytes, [
|
|
31071
31301
|
137,
|
|
31072
31302
|
80,
|
|
31073
31303
|
78,
|
|
@@ -31241,10 +31471,13 @@ const hostModuleId = "ignotum:host";
|
|
|
31241
31471
|
const encodeJavaScriptString = Schema.encodeSync(Schema.fromJsonString(Schema.String));
|
|
31242
31472
|
const DiscoveredServerFunction = Schema.Struct({
|
|
31243
31473
|
address: FunctionAddress,
|
|
31474
|
+
args: Schema.optional(ValueDescriptor),
|
|
31475
|
+
errors: Schema.optional(ValueDescriptor),
|
|
31244
31476
|
exportName: Schema.String,
|
|
31245
31477
|
kind: Schema.Literals(["Mutation", "Query"]),
|
|
31246
31478
|
moduleName: Schema.String,
|
|
31247
|
-
modulePath: Schema.String
|
|
31479
|
+
modulePath: Schema.String,
|
|
31480
|
+
returns: Schema.optional(ValueDescriptor)
|
|
31248
31481
|
});
|
|
31249
31482
|
const ServerDiscoveryManifest = Schema.Struct({
|
|
31250
31483
|
functions: Schema.Array(DiscoveredServerFunction),
|
|
@@ -31328,12 +31561,50 @@ const discoverModuleFunctions = Effect.fn("Deploy.discoverServerModule")(functio
|
|
|
31328
31561
|
});
|
|
31329
31562
|
continue;
|
|
31330
31563
|
}
|
|
31331
|
-
|
|
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 = {
|
|
31332
31591
|
address: FunctionAddress.make(`api.${moduleName}.${exportName}`),
|
|
31333
31592
|
exportName,
|
|
31334
31593
|
kind: inspected.definition._tag,
|
|
31335
31594
|
moduleName,
|
|
31336
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
|
|
31337
31608
|
});
|
|
31338
31609
|
}
|
|
31339
31610
|
return Array$1.sortWith(functions, (definition) => definition.address, String$1.Order);
|
|
@@ -31417,6 +31688,7 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
|
|
|
31417
31688
|
appDirectory,
|
|
31418
31689
|
nodeModulesDirectory(),
|
|
31419
31690
|
packageRoot,
|
|
31691
|
+
path.join(packageRoot, "node_modules"),
|
|
31420
31692
|
...workspaceProbes
|
|
31421
31693
|
].map((allowedPath) => `--allow-fs-read=${allowedPath}`);
|
|
31422
31694
|
const exitCode = yield* spawner.exitCode(ChildProcess.make(process$1.execPath, [
|
|
@@ -31523,12 +31795,24 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (a
|
|
|
31523
31795
|
if (!(yield* fileSystem.exists(absoluteSourceMap))) return yield* InvalidBuildOutput.make({ message: `${definition.address} did not emit a source map.` });
|
|
31524
31796
|
const bundlePath = normalizePath(path.join("server", relativeFile));
|
|
31525
31797
|
const sourceMapPath = normalizePath(path.join("server", relativeSourceMap));
|
|
31526
|
-
|
|
31798
|
+
const artifact = {
|
|
31527
31799
|
address: definition.address,
|
|
31528
31800
|
kind: definition.kind,
|
|
31529
31801
|
bundle: yield* artifactReference(bundlePath, yield* fileSystem.readFile(absoluteFile)),
|
|
31530
31802
|
sourceMap: yield* artifactReference(sourceMapPath, yield* fileSystem.readFile(absoluteSourceMap))
|
|
31531
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
|
+
};
|
|
31532
31816
|
});
|
|
31533
31817
|
const buildServer = Effect.fn("Deploy.buildServer")(function* (appDirectory, outputDirectory, functionModules) {
|
|
31534
31818
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
@@ -31541,7 +31825,7 @@ const buildServer = Effect.fn("Deploy.buildServer")(function* (appDirectory, out
|
|
|
31541
31825
|
yield* fileSystem.writeFile(path.join(outputDirectory, "schema.json"), snapshotBytes);
|
|
31542
31826
|
const functions = yield* Effect.forEach(discovered.functions, (definition) => buildServerFunction(appDirectory, outputDirectory, conditions, definition), { concurrency: 4 });
|
|
31543
31827
|
const manifest = {
|
|
31544
|
-
formatVersion:
|
|
31828
|
+
formatVersion: 2,
|
|
31545
31829
|
schema: yield* artifactReference(schemaSnapshotPath, snapshotBytes),
|
|
31546
31830
|
functions
|
|
31547
31831
|
};
|
|
@@ -31926,7 +32210,7 @@ const make$2 = (options) => Effect$1.gen(function* () {
|
|
|
31926
32210
|
return transformRows ? Effect$1.map(effect, transformRows) : effect;
|
|
31927
32211
|
},
|
|
31928
32212
|
executeStream(_sql, _params) {
|
|
31929
|
-
return Stream.die("executeStream not implemented");
|
|
32213
|
+
return Stream$1.die("executeStream not implemented");
|
|
31930
32214
|
},
|
|
31931
32215
|
backup(destination) {
|
|
31932
32216
|
return Effect$1.suspend(() => {
|
|
@@ -32138,13 +32422,41 @@ const RuntimeQueryResult = Schema.Struct({
|
|
|
32138
32422
|
type: Schema.Literal("Query"),
|
|
32139
32423
|
result: WireResult,
|
|
32140
32424
|
dependencies: DependencySet,
|
|
32141
|
-
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
|
|
32142
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) });
|
|
32143
32454
|
const RuntimeMutationSuccess = Schema.Struct({
|
|
32144
32455
|
type: Schema.Literal("Mutation"),
|
|
32145
32456
|
result: WireSuccess,
|
|
32146
32457
|
invalidations: InvalidationSet,
|
|
32147
|
-
committedRevision: AppStateRevision
|
|
32458
|
+
committedRevision: AppStateRevision,
|
|
32459
|
+
fileEffects: Schema.optional(FileEffectBatch)
|
|
32148
32460
|
});
|
|
32149
32461
|
const RuntimeMutationFailure = Schema.Struct({
|
|
32150
32462
|
type: Schema.Literal("Mutation"),
|
|
@@ -32167,7 +32479,8 @@ const QueryInvocation = Schema.Struct({
|
|
|
32167
32479
|
const MutationInvocation = Schema.Struct({
|
|
32168
32480
|
...InvocationBase,
|
|
32169
32481
|
type: Schema.Literal("Mutation"),
|
|
32170
|
-
invocationId: InvocationId
|
|
32482
|
+
invocationId: InvocationId,
|
|
32483
|
+
files: Schema.optional(Schema.Array(RuntimeFileOccurrence))
|
|
32171
32484
|
});
|
|
32172
32485
|
Schema.Union([QueryInvocation, MutationInvocation]);
|
|
32173
32486
|
Schema.Struct({
|
|
@@ -32256,6 +32569,75 @@ const indexPointInvalidation = (tableId, index, key) => ({
|
|
|
32256
32569
|
//#region ../runtime/dist/functions.js
|
|
32257
32570
|
var FunctionRuntime = class extends Context.Service()("@ignotum/runtime/functions/FunctionRuntime") {};
|
|
32258
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
|
|
32259
32641
|
//#region ../runtime/dist/sync.js
|
|
32260
32642
|
const makeDependencyIndex = () => {
|
|
32261
32643
|
const dependenciesByQuery = MutableHashMap.empty();
|
|
@@ -32326,55 +32708,6 @@ const makeDependencyIndex = () => {
|
|
|
32326
32708
|
};
|
|
32327
32709
|
};
|
|
32328
32710
|
var QueryInvalidation = class extends Context.Service()("@ignotum/runtime/sync/QueryInvalidation") {};
|
|
32329
|
-
//#endregion
|
|
32330
|
-
//#region ../contracts/dist/runtime/transport.js
|
|
32331
|
-
const runtimeInvocationPath = "/v1/invoke";
|
|
32332
|
-
const runtimeRevisionPath = "/v1/revision";
|
|
32333
|
-
const runtimeIndexPreparePath = "/v1/indexes/prepare";
|
|
32334
|
-
const runtimeIndexCommitPath = "/v1/indexes/commit";
|
|
32335
|
-
const appSyncPath = `/_ignotum/v1/sync`;
|
|
32336
|
-
const RuntimeRequestPath = Schema.Literals([
|
|
32337
|
-
runtimeInvocationPath,
|
|
32338
|
-
runtimeRevisionPath,
|
|
32339
|
-
runtimeIndexPreparePath,
|
|
32340
|
-
runtimeIndexCommitPath
|
|
32341
|
-
]);
|
|
32342
|
-
const RuntimeRequestTimestamp = Schema.FiniteFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ignotum/runtime/RequestTimestamp"));
|
|
32343
|
-
const RuntimeRequestSignature = Sha256.pipe(Schema.brand("ignotum/runtime/RequestSignature"));
|
|
32344
|
-
Schema.Struct({
|
|
32345
|
-
appId: AppId,
|
|
32346
|
-
deploymentId: Schema.optional(DeploymentId),
|
|
32347
|
-
nonce: RuntimeRequestNonce,
|
|
32348
|
-
timestamp: RuntimeRequestTimestamp,
|
|
32349
|
-
signature: RuntimeRequestSignature
|
|
32350
|
-
});
|
|
32351
|
-
Schema.Struct({
|
|
32352
|
-
method: Schema.Literal("POST"),
|
|
32353
|
-
path: RuntimeRequestPath,
|
|
32354
|
-
timestamp: RuntimeRequestTimestamp,
|
|
32355
|
-
nonce: RuntimeRequestNonce,
|
|
32356
|
-
appId: AppId,
|
|
32357
|
-
deploymentId: Schema.optional(DeploymentId),
|
|
32358
|
-
bodySha256: Sha256
|
|
32359
|
-
});
|
|
32360
|
-
Schema.Struct({
|
|
32361
|
-
appId: AppId,
|
|
32362
|
-
deploymentId: DeploymentId
|
|
32363
|
-
});
|
|
32364
|
-
Schema.Struct({ completed: Schema.Literal(true) });
|
|
32365
|
-
const RuntimeErrorCode = Schema.Literals([
|
|
32366
|
-
"InvalidRequest",
|
|
32367
|
-
"InvalidSignature",
|
|
32368
|
-
"RequestExpired",
|
|
32369
|
-
"RequestReplay",
|
|
32370
|
-
"InvocationRejected",
|
|
32371
|
-
"RuntimeUnavailable"
|
|
32372
|
-
]);
|
|
32373
|
-
Schema.Struct({
|
|
32374
|
-
code: RuntimeErrorCode,
|
|
32375
|
-
message: Schema.String,
|
|
32376
|
-
rejectionCode: Schema.optional(RuntimeInvocationRejectionCode)
|
|
32377
|
-
});
|
|
32378
32711
|
const syncPath = appSyncPath;
|
|
32379
32712
|
const isIgnotumPath = (pathname) => pathname === "/_ignotum" || pathname.startsWith(`/_ignotum/`);
|
|
32380
32713
|
//#endregion
|
|
@@ -32450,9 +32783,21 @@ const addDevelopmentIndexes = Effect.gen(function* () {
|
|
|
32450
32783
|
ON indexEntries (indexId, key)
|
|
32451
32784
|
`;
|
|
32452
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
|
+
});
|
|
32453
32797
|
const developmentMigrationLoader = Migrator.fromRecord({
|
|
32454
32798
|
"0001_initial_development_schema": initialDevelopmentSchema,
|
|
32455
|
-
"0002_application_indexes": addDevelopmentIndexes
|
|
32799
|
+
"0002_application_indexes": addDevelopmentIndexes,
|
|
32800
|
+
"0003_application_files": addApplicationFiles
|
|
32456
32801
|
});
|
|
32457
32802
|
var DevelopmentDatabase = class DevelopmentDatabase extends Context.Service()("ignotum/dev-runtime/migrations/DevelopmentDatabase") {
|
|
32458
32803
|
static layer = Layer.effect(DevelopmentDatabase, Migrator.make({})({
|
|
@@ -32846,68 +33191,65 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
32846
33191
|
return id;
|
|
32847
33192
|
});
|
|
32848
33193
|
const deleteDocument = Effect.fn("LocalDatabase.delete")(function* (schema, tableName, id, invalidations) {
|
|
32849
|
-
const table = yield* resolveTable(schema, tableName);
|
|
33194
|
+
const table = yield* resolveTable(schema, tableName).pipe(Effect.orDie);
|
|
32850
33195
|
const documentId = GeneratedId.make(id);
|
|
32851
33196
|
const stored = yield* findStored({
|
|
32852
33197
|
id,
|
|
32853
33198
|
tableId: table.id
|
|
32854
|
-
});
|
|
32855
|
-
|
|
32856
|
-
|
|
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);
|
|
32857
33203
|
invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId), ...keys.map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
|
|
32858
33204
|
});
|
|
32859
33205
|
const patch = Effect.fn("LocalDatabase.patch")(function* (schema, tableName, id, value, invalidations) {
|
|
32860
|
-
const table = yield* resolveTable(schema, tableName);
|
|
33206
|
+
const table = yield* resolveTable(schema, tableName).pipe(Effect.orDie);
|
|
32861
33207
|
const documentId = GeneratedId.make(id);
|
|
32862
33208
|
const stored = yield* findStored({
|
|
32863
33209
|
id,
|
|
32864
33210
|
tableId: table.id
|
|
32865
|
-
});
|
|
32866
|
-
|
|
32867
|
-
|
|
32868
|
-
|
|
32869
|
-
|
|
32870
|
-
|
|
32871
|
-
|
|
32872
|
-
|
|
32873
|
-
|
|
32874
|
-
|
|
32875
|
-
|
|
32876
|
-
|
|
32877
|
-
|
|
32878
|
-
|
|
32879
|
-
|
|
32880
|
-
|
|
32881
|
-
|
|
32882
|
-
|
|
32883
|
-
|
|
32884
|
-
});
|
|
32885
|
-
}
|
|
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);
|
|
32886
33230
|
invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId), ...[...oldKeys, ...newKeys].map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
|
|
32887
33231
|
});
|
|
32888
33232
|
const replace = Effect.fn("LocalDatabase.replace")(function* (schema, tableName, id, value, invalidations) {
|
|
32889
|
-
const table = yield* resolveTable(schema, tableName);
|
|
33233
|
+
const table = yield* resolveTable(schema, tableName).pipe(Effect.orDie);
|
|
32890
33234
|
const documentId = GeneratedId.make(id);
|
|
32891
33235
|
const stored = yield* findStored({
|
|
32892
33236
|
id,
|
|
32893
33237
|
tableId: table.id
|
|
32894
|
-
});
|
|
32895
|
-
|
|
32896
|
-
|
|
32897
|
-
|
|
32898
|
-
|
|
32899
|
-
|
|
32900
|
-
|
|
32901
|
-
|
|
32902
|
-
|
|
32903
|
-
|
|
32904
|
-
|
|
32905
|
-
|
|
32906
|
-
|
|
32907
|
-
|
|
32908
|
-
|
|
32909
|
-
});
|
|
32910
|
-
}
|
|
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);
|
|
32911
33253
|
invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId), ...[...oldKeys, ...newKeys].map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
|
|
32912
33254
|
});
|
|
32913
33255
|
const makeReader = (schema, dependencies) => {
|
|
@@ -33021,10 +33363,10 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
33021
33363
|
const reader = makeReader(schema);
|
|
33022
33364
|
return Object.freeze({
|
|
33023
33365
|
...reader,
|
|
33024
|
-
delete: (tableName, id) => resultFromEffect(deleteDocument(schema, tableName, id, invalidations)
|
|
33366
|
+
delete: (tableName, id) => resultFromEffect(deleteDocument(schema, tableName, id, invalidations)),
|
|
33025
33367
|
insert: (tableName, value) => resultFromEffect(insert(schema, tableName, value, invalidations).pipe(Effect.orDie)),
|
|
33026
|
-
patch: (tableName, id, value) => resultFromEffect(patch(schema, tableName, id, value, invalidations)
|
|
33027
|
-
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))
|
|
33028
33370
|
});
|
|
33029
33371
|
};
|
|
33030
33372
|
const trackedQueryTransaction = (schema, use) => sql.withTransaction(Effect.gen(function* () {
|
|
@@ -33135,12 +33477,19 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
|
|
|
33135
33477
|
onFailure: (error) => encodeFailure(resolved.definition, error),
|
|
33136
33478
|
onSuccess: (value) => encodeSuccess(resolved.definition, value)
|
|
33137
33479
|
}));
|
|
33138
|
-
return yield* (kind === "Query" ? database.trackedQueryTransaction(resolved.schema, invoke).pipe(Effect.map(({ dependencies, observedRevision, value }) =>
|
|
33139
|
-
type
|
|
33140
|
-
result
|
|
33141
|
-
|
|
33142
|
-
|
|
33143
|
-
|
|
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 }) => ({
|
|
33144
33493
|
type: "Mutation",
|
|
33145
33494
|
result: value,
|
|
33146
33495
|
invalidations,
|
|
@@ -33189,6 +33538,252 @@ const functionRuntimeLayer = Layer.effect(FunctionRuntime, Effect.gen(function*
|
|
|
33189
33538
|
}) });
|
|
33190
33539
|
}));
|
|
33191
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
|
|
33192
33787
|
//#region src/dev-runtime/sync.ts
|
|
33193
33788
|
const localSyncIdentity = {
|
|
33194
33789
|
appId: AppId.make("app_000000000000000000000001"),
|
|
@@ -33266,6 +33861,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
33266
33861
|
const runtime = yield* FunctionRuntime;
|
|
33267
33862
|
const invalidation = yield* QueryInvalidation;
|
|
33268
33863
|
const mutationReplay = yield* MutationReplay;
|
|
33864
|
+
const files = yield* LocalApplicationFiles;
|
|
33269
33865
|
const subscriptions = yield* Ref.make(HashMap.empty());
|
|
33270
33866
|
const dependencyIndex = makeDependencyIndex();
|
|
33271
33867
|
const invocationFibers = yield* FiberSet.make();
|
|
@@ -33304,11 +33900,16 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
33304
33900
|
if (!(yield* isActive(subscriptionId, subscription)) || subscription.refreshGeneration !== refreshGeneration) return;
|
|
33305
33901
|
dependencyIndex.record(subscriptionId, result.dependencies);
|
|
33306
33902
|
if ((yield* invalidation.latestRevision) > result.observedRevision) continue;
|
|
33307
|
-
yield*
|
|
33903
|
+
const granted = yield* files.grants(subscriptionId, result.files ?? []);
|
|
33904
|
+
const snapshot = {
|
|
33308
33905
|
type: "Snapshot",
|
|
33309
33906
|
id: subscriptionId,
|
|
33310
33907
|
result: result.result,
|
|
33311
33908
|
revision: result.observedRevision
|
|
33909
|
+
};
|
|
33910
|
+
yield* deliver(granted.length === 0 ? snapshot : {
|
|
33911
|
+
...snapshot,
|
|
33912
|
+
files: granted
|
|
33312
33913
|
});
|
|
33313
33914
|
return;
|
|
33314
33915
|
}
|
|
@@ -33336,6 +33937,10 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
33336
33937
|
yield* PubSub.take(invalidations).pipe(Effect.flatMap((event) => event.type === "All" ? refreshAll() : refreshAffected(event.invalidations)), Effect.forever, Effect.forkScoped);
|
|
33337
33938
|
const handleSubscribe = Effect.fn("SyncServer.handleSubscribe")(function* (message) {
|
|
33338
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
|
+
}
|
|
33339
33944
|
const current = yield* Ref.get(subscriptions);
|
|
33340
33945
|
if (HashMap.has(current, message.id)) {
|
|
33341
33946
|
yield* send(protocolError("DuplicateOperationId", `Subscription ${message.id} already exists.`, operation));
|
|
@@ -33362,13 +33967,20 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
33362
33967
|
type: "Invocation",
|
|
33363
33968
|
id: message.id
|
|
33364
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
|
+
}
|
|
33365
33974
|
const prepared = yield* runtime.prepare(message.function, "Mutation", message.args).pipe(Effect.catchTags({
|
|
33366
33975
|
FunctionUnavailable: (error) => sendResolutionError(operation, error),
|
|
33367
33976
|
InvalidArguments: (error) => sendResolutionError(operation, error),
|
|
33368
33977
|
UnknownFunction: (error) => sendResolutionError(operation, error),
|
|
33369
33978
|
WrongFunctionKind: (error) => sendResolutionError(operation, error)
|
|
33370
33979
|
}));
|
|
33371
|
-
if (prepared === void 0)
|
|
33980
|
+
if (prepared === void 0) {
|
|
33981
|
+
yield* files.releasePreparation(message.id);
|
|
33982
|
+
return;
|
|
33983
|
+
}
|
|
33372
33984
|
const input = canonicalInvocationInput(localSyncIdentity.deploymentId, "Mutation", message.function, message.args);
|
|
33373
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) => {
|
|
33374
33986
|
if (!("committedRevision" in result)) return send({
|
|
@@ -33386,12 +33998,23 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
33386
33998
|
invalidations: result.invalidations,
|
|
33387
33999
|
committedRevision: result.committedRevision
|
|
33388
34000
|
})));
|
|
33389
|
-
}), 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({
|
|
33390
34002
|
cause,
|
|
33391
34003
|
function: message.function,
|
|
33392
34004
|
requestId: message.id
|
|
33393
34005
|
}))), FiberSet.run(invocationFibers), Effect.asVoid);
|
|
33394
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
|
+
});
|
|
33395
34018
|
const handleMessage = Effect.fn("SyncServer.handleMessage")(function* (text) {
|
|
33396
34019
|
const message = yield* Schema.decodeEffect(ClientMessageJson)(text).pipe(Effect.catch(() => send(protocolError("InvalidMessage", "The WebSocket frame is not valid Ignotum JSON."))));
|
|
33397
34020
|
if (message === void 0) return;
|
|
@@ -33402,6 +34025,10 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
33402
34025
|
case "Unsubscribe":
|
|
33403
34026
|
dependencyIndex.remove(message.id);
|
|
33404
34027
|
yield* Ref.update(subscriptions, HashMap.remove(message.id));
|
|
34028
|
+
yield* files.releaseGrants(message.id);
|
|
34029
|
+
return;
|
|
34030
|
+
case "PrepareMutation":
|
|
34031
|
+
yield* handlePrepareMutation(message);
|
|
33405
34032
|
return;
|
|
33406
34033
|
case "Invoke":
|
|
33407
34034
|
yield* handleInvoke(message);
|
|
@@ -33411,7 +34038,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
33411
34038
|
yield* socket.runString((text) => messageSemaphore.withPermits(1)(handleMessage(text)), { onOpen: send({
|
|
33412
34039
|
type: "Handshake",
|
|
33413
34040
|
...localSyncIdentity
|
|
33414
|
-
}).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)));
|
|
33415
34042
|
});
|
|
33416
34043
|
var SyncHandlers = class extends Context.Service()("ignotum/dev-runtime/sync/SyncHandlers") {};
|
|
33417
34044
|
const makeHandlersLayer = (server, appDirectory, databasePath) => {
|
|
@@ -33419,7 +34046,7 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
|
|
|
33419
34046
|
const persistenceLayer = Layer.merge(LocalDatabase.layer, mutationReplayLayer).pipe(Layer.provideMerge(DevelopmentDatabase.layer), Layer.provide(Layer.merge(idGeneratorLayer, sqliteLayer)));
|
|
33420
34047
|
const executorLayer = FunctionExecutor.layer.pipe(Layer.provide(persistenceLayer), Layer.provide(idGeneratorLayer));
|
|
33421
34048
|
const devFunctionRuntimeLayer = functionRuntimeLayer.pipe(Layer.provide(Layer.merge(FunctionRegistry.layer(server, appDirectory).pipe(Layer.provide(layer$1)), executorLayer)));
|
|
33422
|
-
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))));
|
|
33423
34050
|
return Layer.effect(SyncHandlers, Effect.gen(function* () {
|
|
33424
34051
|
const invalidation = yield* QueryInvalidation;
|
|
33425
34052
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
@@ -33435,6 +34062,30 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
|
|
|
33435
34062
|
const httpApp = Effect.gen(function* () {
|
|
33436
34063
|
const request = yield* HttpServerRequest$1.HttpServerRequest;
|
|
33437
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
|
+
}
|
|
33438
34089
|
return yield* HttpServerResponse.json(pathname === syncPath ? {
|
|
33439
34090
|
code: "UpgradeRequired",
|
|
33440
34091
|
message: `Connect to ${syncPath} with WebSocket.`
|
|
@@ -33614,30 +34265,58 @@ const dev$1 = Effect.fn("Dev.run")(function* (options) {
|
|
|
33614
34265
|
return yield* Effect.never;
|
|
33615
34266
|
}));
|
|
33616
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/, "");
|
|
33617
34309
|
const agentAppFiles = [
|
|
33618
34310
|
{
|
|
33619
|
-
content:
|
|
34311
|
+
content: AGENTS_default,
|
|
33620
34312
|
path: "AGENTS.md"
|
|
33621
34313
|
},
|
|
33622
34314
|
{
|
|
33623
|
-
content:
|
|
34315
|
+
content: SKILL_default,
|
|
33624
34316
|
path: ".agents/skills/ignotum/SKILL.md"
|
|
33625
34317
|
},
|
|
33626
|
-
...[
|
|
33627
|
-
|
|
33628
|
-
["deploy.md", "# Deploy\n\nSet your API token in your shell:\n\n```sh\nexport IGNOTUM_API_TOKEN=your-token\n```\n\nDo not put the token in the app directory or commit it to source control.\n\nOn the first deploy, pass the app slug:\n\n```sh\nnpx ignotum deploy --app my-app\n```\n\nIf the slug belongs to one of your apps, Ignotum links it. Otherwise, Ignotum creates the app.\nWithout `--app`, the first deploy asks for the slug interactively.\n\nIgnotum stores the app ID, slug, and API URL in `.ignotum/app.json`. It never stores the access\ntoken there. Later deploys reuse the saved app, so the command has no app flag:\n\n```sh\nnpx ignotum deploy\n```\n\nThere is no separate build command. `deploy` generates the current bindings and builds both parts\nof the application, uploads the artifact, and makes the new deployment active.\n\nThe client output contains the SPA shell, generated assets, and validated files from the top-level\n`public` directory. The server output contains one bundle for every exported query or mutation.\nIgnotum writes both under `.ignotum/build` only after the complete build succeeds. If a rebuild\nfails, the previous successful output stays in place.\n\nUploads stream from disk. If an upload is interrupted after it begins, Ignotum leaves the active\ndeployment unchanged and prints the incomplete deployment ID. A rejected upload reports the\nAPI's reason. A successful deploy prints the app URL and deployment ID.\n\nThe CLI uses `https://api.ignotum.cloud` by default. Set `IGNOTUM_API_URL` only when targeting a\ndifferent Ignotum API, such as a local or development deployment.\n\nIgnotum rejects deployments that exceed the hosted file or artifact size limits. It also removes\nunfinished uploads and older inactive deployments after their retention periods. See\n[Limits](limits.md) for the current values.\n"],
|
|
33629
|
-
["dev-server.md", "# Dev server\n\nRun the dev server from the app root:\n\n```sh\nnpx ignotum dev\n```\n\nIt expects these files:\n\n```text\nclient/index.tsx\nserver/schema.ts\n```\n\nThe dev server generates the client and server bindings, reads the `app(...)` definition from\n`client/index.tsx`, loads Tailwind automatically, and serves the app at\n<http://127.0.0.1:3210>. It reloads client and server changes and updates active queries after\nserver changes and successful mutations.\n\nYou do not need an HTML file, Vite configuration, Tailwind configuration, or framework stylesheet.\nAdd an optional `client/icon.svg` and Ignotum uses it as the favicon automatically. With no icon\nfile, the HTML contains no favicon link.\n\n## Flags\n\nUse flags to change the address or open the browser:\n\n```sh\nnpx ignotum dev --host 0.0.0.0 --port 3000 --open\n```\n\nThe defaults are host `127.0.0.1`, port `3210`, and no automatic browser opening.\n\n## Code generation\n\nThe dev server runs code generation when it starts. It updates generated files when you add or\nremove a server function file.\n\nRun code generation before typechecking without the dev server:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nIgnotum creates:\n\n- `_generated/server.ts` with schema-bound `query`, `mutation`, and `values` exports;\n- `_generated/api.ts` with client references such as `api.todos.list`;\n- `_generated/types.ts` with `DataModel`, `Doc`, and `Id`.\n\nDo not edit generated files.\n\n## Local data\n\nData persists between dev-server restarts. Stop the server and reset that data with:\n\n```sh\nnpx ignotum dev db reset\n```\n"],
|
|
33630
|
-
["getting-started.md", "# Getting started\n\nIgnotum requires Node.js 22.18 or newer.\n\nCreate an app:\n\n```sh\nnpx ignotum new my-app\ncd my-app\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. The generated app is a small counter with a schema, a query, a\nmutation, and a JSX client.\n\n`ignotum new` installs dependencies with pnpm when it is available. It falls back to npm only when\npnpm is not installed. It generates `_generated`, initializes a Git repository, and creates an\n`Init` commit containing the generated files after the rest of the setup finishes.\n\nPass `.` to create the app in the current directory. The directory must be empty:\n\n```sh\nnpx ignotum new .\n```\n\nUse `--no-git` to skip Git or `--no-install` to skip dependency installation. You can install the\ndependencies later with the same pnpm and npm fallback behavior:\n\n```sh\nnpx ignotum install\n```\n\nRead [schema syntax](schema.md), [server functions](server-functions.md),\n[database reads](reading-data.md), [database writes](writing-data.md), and the\n[client guide](client.md) to build the app. The [manual setup](manual-setup.md) recreates the counter\napp without `ignotum new`. When it is ready, follow the [deploy guide](deploy.md) to create or link\nthe hosted app and publish it.\n"],
|
|
33631
|
-
["index.md", "# Ignotum\n\nAn Ignotum app has a schema, server functions, and a client. Ignotum is opinionated about the\nclient tooling. Every app uses JSX, hooks from `ignotum/client`, and Tailwind CSS.\n\nThe current release supports local development and hosted deployment.\n\n- [Getting started](getting-started.md) creates and runs a counter app with `ignotum new`.\n- [Manual setup](manual-setup.md) recreates the generated counter app by hand.\n- [Schema syntax](schema.md) covers tables, fields, IDs, and generated document types.\n- [Values](values.md) lists every value validator and its TypeScript type.\n- [Server functions](server-functions.md) covers queries, mutations, and application errors.\n- [Read from the database](reading-data.md) covers documents, indexes, ranges, ordering, and result methods.\n- [Write to the database](writing-data.md) covers inserts, patches, replacements, deletes, and mutation behavior.\n- [Client](client.md) covers queries, mutations, results, and Tailwind styling.\n- [Dev server](dev-server.md) covers local development, code generation, flags, and data reset.\n- [Deploy](deploy.md) creates or links an app, then uploads and activates a deployment.\n- [Limits](limits.md) lists hosted limits for functions, data, realtime updates, and deployments.\n"],
|
|
33632
|
-
["limits.md", "# Limits\n\nThese limits apply to hosted Ignotum apps. The local dev server does not reproduce every hosted\nlimit, so an operation that works locally can still be rejected after deployment.\n\nIgnotum reports connection, traffic, time, result, and storage-quota failures as\n`ResourceLimitExceeded`. A database read or write that crosses a document or collection limit can\nappear as a temporary function failure. A deployment that exceeds a limit fails before activation,\nso the current deployment stays active.\n\n## Server functions\n\n| Limit | Value |\n| --------------------------------------- | ---------: |\n| Function arguments | 16 KiB |\n| Function result | 1 MiB |\n| Execution time | 10 seconds |\n| Memory | 32 MiB |\n| Stack | 512 KiB |\n| Ignotum operations during one execution | 1,000 |\n\nArgument and result sizes use their JSON representation. The result limit applies to successful\nresults and application errors.\n\nAn Ignotum operation is a call through the function context, such as a database read or write. A\nfunction stops when it reaches the execution time or operation limit.\n\nIgnotum retries one query execution when app data changes while it runs. That execution makes at\nmost four attempts and stops after 15 seconds. The query fails temporarily if it cannot read a\nconsistent result in that time.\n\n## App data\n\n| Limit | Value |\n| --------------------------------------- | -------------: |\n| Stored fields in one document | 256 KiB |\n| Documents returned by `collect()` | 1,000 |\n| Documents returned by `take()` | 1,000 |\n| Documents returned by one page | 1,000 |\n| Document fields returned by `collect()` | 1 MiB |\n| Stored app data | 64 MiB per app |\n\nThe 1 MiB function-result limit still applies to `collect()`. Document IDs and timestamps take some\nspace in that result, so a collection can reach the result limit before its fields reach 1 MiB.\n\nIf a mutation would take the app over its storage limit, Ignotum rolls back the whole mutation.\nThe quota covers stored documents, application indexes, and the records Ignotum keeps to process\nmutations safely. Deployment files do not count as stored app data.\n\n## Schema indexes\n\n| Limit | Value |\n| ---------------------------- | -------------: |\n| Indexes on one table | 16 |\n| Indexes in one schema | 128 |\n| Declared fields in one index | 8 |\n| Index name | 64 UTF-8 bytes |\n| One encoded index key | 4 KiB |\n\nEvery index also includes `createdAt` and `id` after its declared fields. The key limit applies to\nthe combined encoded field values and those final ordering values. A document must fit every index\ndeclared for its table.\n\n## Realtime connections and calls\n\n| Limit | Value |\n| ------------------------- | ---------------------------: |\n| Live connections | 256 per app |\n| Incoming realtime message | 64 KiB |\n| Subscriptions | 128 per connection |\n| Active unique queries | 64 per app |\n| Realtime query refresh | 4 attempts within 15 seconds |\n| Concurrent operations | 32 per app |\n| Mutation execution | 1 at a time per app |\n| Unresolved mutations | 32 per app |\n| Mutation calls | 60 per minute per app |\n\nA unique query is one function and argument combination. Several components or browser tabs can\nsubscribe to the same unique query without using another unique-query slot.\n\nA realtime refresh can repeat a query execution when app data changes again during the refresh.\n\nThe unresolved-mutation limit protects calls whose outcome is not known yet, such as a call waiting\nfor a retry after a connection failure. Once Ignotum receives a final result, that call no longer\ncounts toward the limit.\n\nIgnotum runs mutations for the same app one at a time. Queries and other live calls can still use\nthe remaining concurrent-operation slots.\n\n## Mutation retries\n\nThe client keeps the ID of a pending mutation and reuses it after a reconnect. Ignotum remembers up\nto 10,000 mutation results per app for seven days. During that period, a retry returns the recorded\nresult instead of running the mutation again.\n\nDo not treat this as permanent duplicate protection. A mutation may run again after its record has\nexpired or fallen outside the 10,000 most recent results.\n\n## Deployments\n\n| Limit | Value |\n| ------------------------------ | ------: |\n| Files listed in one deployment | 512 |\n| One listed file | 16 MiB |\n| Deployment inventory | 1 MiB |\n| Server files combined | 64 MiB |\n| Listed files combined | 128 MiB |\n\nIgnotum never deletes the active deployment. It also protects the three newest deployments that\ncompleted successfully. An older inactive deployment becomes eligible for deletion after seven\ndays. An unfinished upload becomes eligible after 24 hours.\n"],
|
|
33633
|
-
["manual-setup.md", "# Manual setup\n\nIgnotum requires Node.js 22.18 or newer. This guide recreates the counter app from `ignotum new`\nwithout running the app generator. It uses pnpm to install dependencies.\n\nCreate the app directory:\n\n```sh\nmkdir my-ignotum-app\ncd my-ignotum-app\n```\n\nCreate this structure:\n\n```text\nmy-ignotum-app/\n client/\n index.tsx\n icon.svg\n public/\n manual.pdf\n server/\n counter.ts\n schema.ts\n shared/\n utils.ts\n package.json\n tsconfig.json\n```\n\n## Configure the package\n\nAdd `package.json`:\n\n```json\n{\n \"name\": \"my-ignotum-app\",\n \"private\": true,\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"typecheck\": \"ignotum codegen && tsc --noEmit\"\n },\n \"dependencies\": {\n \"ignotum\": \"latest\"\n },\n \"devDependencies\": {\n \"typescript\": \"^7.0.2\"\n },\n \"engines\": {\n \"node\": \">=22.18.0\"\n }\n}\n```\n\nInstall the dependencies:\n\n```sh\npnpm install\n```\n\n## Configure TypeScript\n\nAdd `tsconfig.json`:\n\n```json\n{\n \"compilerOptions\": {\n \"target\": \"ES2023\",\n \"lib\": [\"ES2023\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"NodeNext\",\n \"moduleResolution\": \"NodeNext\",\n \"jsx\": \"react-jsx\",\n \"jsxImportSource\": \"ignotum/client\",\n \"strict\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true,\n \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"_generated\", \"client\", \"server\", \"shared\"]\n}\n```\n\n## Add shared code\n\nAdd `shared/utils.ts`:\n\n```ts\nexport const counterIncrement = 1;\n```\n\nBoth the client and server can import files in `shared` through `@/shared`.\n\n## Define the schema\n\nAdd `server/schema.ts`:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n counters: table({\n value: values.number(),\n }),\n}));\n```\n\n## Add the server functions\n\nAdd `server/counter.ts`:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\nimport { counterIncrement } from \"@/shared/utils.js\";\n\nexport const get = query({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n return counters[0]?.value ?? 0;\n },\n});\n\nexport const increment = mutation({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n const counter = counters[0];\n const value = (counter?.value ?? 0) + counterIncrement;\n\n if (counter === undefined) {\n yield* ctx.db.insert(\"counters\", { value });\n } else {\n yield* ctx.db.patch(\"counters\", counter.id, { value });\n }\n\n return value;\n },\n});\n```\n\nKeep the `.js` suffix on imports from `@/_generated`, even though the generated files use\nTypeScript.\n\n## Add the client\n\nAdd `client/index.tsx`:\n\n```tsx\nimport { app, Result, useMutation, useQuery } from \"ignotum/client\";\n\nimport { api } from \"@/_generated/api.js\";\nimport { counterIncrement } from \"@/shared/utils.js\";\n\nfunction App() {\n const count = useQuery(api.counter.get);\n const increment = useMutation(api.counter.increment);\n\n return (\n <main class=\"mx-auto max-w-sm px-6 py-20 text-center\">\n <h1 class=\"text-2xl font-semibold\">Counter</h1>\n {Result.match(count, {\n pending: () => <p class=\"mt-6\">Loading...</p>,\n value: (value) => (\n <>\n <p class=\"my-6 text-5xl tabular-nums\">{value}</p>\n <button\n class=\"rounded bg-zinc-900 px-4 py-2 text-white\"\n type=\"button\"\n onClick={() => void increment()}\n >\n Increment by {counterIncrement}\n </button>\n </>\n ),\n })}\n </main>\n );\n}\n\nexport default app({\n title: \"Counter\",\n component: App,\n});\n```\n\nIgnotum loads Tailwind automatically. You do not need an HTML file, Vite configuration, Tailwind\nconfiguration, or framework stylesheet. Custom CSS files are ordinary client modules and can use\nany filename when imported from app code.\n\nThe `client/icon.svg` file is optional. When present, Ignotum discovers it automatically and uses it\nas the favicon. When absent, the app has no favicon link.\n\nThe top-level `public` directory is optional. Its AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF files keep\ntheir relative paths as public URLs. Do not put this directory inside `client`.\n\n## Run the app\n\nStart the dev server:\n\n```sh\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. Ignotum creates `_generated` before starting the app.\n\nRead [schema syntax](schema.md), [server functions](server-functions.md),\n[database reads](reading-data.md), [database writes](writing-data.md), and the\n[client guide](client.md) to continue building the app.\n"],
|
|
33634
|
-
["reading-data.md", "# Read from the database\n\nQuery and mutation handlers can read documents through `ctx.db`. Every read is explicit: a table\nquery reads that table, while an indexed query follows an index declared in the schema. Ignotum\ndoes not silently load a table and filter it to make an unindexed operation look efficient.\n\n## Read one document\n\nUse `find` when a missing document is an ordinary result:\n\n```ts\nconst todo = yield * ctx.db.find(\"todos\", args.id);\n// Todo | undefined\n```\n\nUse `get` when the document should exist:\n\n```ts\nconst todo = yield * ctx.db.get(\"todos\", args.id);\n// Todo\n```\n\nA missing `get` fails with `DocumentNotFound`, which contains the table and ID. You can expose it,\ncatch it, or map it to one of your own application errors.\n\n## Read a table\n\n`collect` returns every document in the query:\n\n```ts\nconst todos = yield * ctx.db.query(\"todos\").collect();\n```\n\nThis is the right API when the table is intentionally small or the function genuinely needs every\ndocument. It is a full table read, not an indexed lookup. Hosted collection limits still apply.\n\nQueries use ascending `createdAt` order by default, with `id` breaking ties. Reverse the order with\n`order(\"desc\")`:\n\n```ts\nconst newestFirst = yield * ctx.db.query(\"todos\").order(\"desc\").take(20);\n```\n\n## Read through an index\n\nDeclare indexes in the schema, then select one by name:\n\n```ts\n// server/schema.ts\ntodos: table({\n channel: values.string(),\n priority: values.integer(),\n text: values.string(),\n}).index(\"by_channel_priority\", [\"channel\", \"priority\"]);\n```\n\n```ts\nconst todos =\n yield *\n ctx.db\n .query(\"todos\")\n .index(\"by_channel_priority\", (range) =>\n range.eq(\"channel\", args.channel).gte(\"priority\", args.minimumPriority),\n )\n .collect();\n```\n\nRange fields must follow the index order. Use equality on any leading fields, then optionally add a\nlower bound, an upper bound, or both on the next field:\n\n| Method | Matches |\n| ------------------- | -------------------------------- |\n| `eq(field, value)` | Equal to `value` |\n| `gt(field, value)` | Greater than `value` |\n| `gte(field, value)` | Greater than or equal to `value` |\n| `lt(field, value)` | Less than `value` |\n| `lte(field, value)` | Less than or equal to `value` |\n\nFor a two-sided range, chain the bounds on the same field:\n\n```ts\nconst thisWeek =\n yield *\n ctx.db\n .query(\"events\")\n .index(\"by_workspace_start\", (range) =>\n range\n .eq(\"workspaceId\", args.workspaceId)\n .gte(\"start\", args.weekStart)\n .lt(\"start\", args.nextWeek),\n )\n .collect();\n```\n\nCalling `index` without a range scans that index in its declared order:\n\n```ts\nconst byPriority = yield * ctx.db.query(\"todos\").index(\"by_priority\").collect();\n```\n\nEvery index orders by its declared fields, then `createdAt`, then `id`. The two system fields are\navailable as range fields after all declared fields have been matched with `eq`.\n\n## Choose how many documents to return\n\nAll table and index queries support these terminal methods:\n\n| Method | Result |\n| ------------------- | ---------------------------------------------------------------------- |\n| `collect()` | Every matching document |\n| `take(number)` | At most `number` matching documents |\n| `first()` | The first matching document, or `undefined` |\n| `unique()` | The only matching document, `undefined`, or a failure if several match |\n| `paginate(options)` | One page and a cursor for the next page |\n\n`unique()` checks a query result; it does not add a uniqueness constraint to the index. Use\n`first()` when several matches are valid and only the first one matters.\n\nUse `take` or `first` whenever the function only needs a bounded result. Use `collect` when reading\nthe complete matching set is intentional.\n\n## Read pages\n\nAdd a `pagination` argument and return a page validator when a client should load a long ordered\nlist in parts:\n\n```ts\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n args: {\n completed: values.boolean(),\n pagination: values.pagination(),\n },\n returns: values.page(Todo),\n\n handler: function* (ctx, { completed, pagination }) {\n return yield* ctx.db\n .query(\"todos\")\n .index(\"by_completed\", (range) => range.eq(\"completed\", completed))\n .order(\"desc\")\n .paginate(pagination);\n },\n});\n```\n\nA page contains `items` and `nextCursor`. A `null` cursor means there are no more matching\ndocuments. Pass a non-null cursor back through `pagination` to continue the same table, index,\nrange, and order. Cursors are opaque and must not be parsed or changed.\n\nPage sizes are integers from 1 through 1,000. Prefer an index for paginated filters so the database\ncan read the requested range directly.\n\n## Reads in mutations\n\nMutation handlers receive the same read API and observe their earlier writes in that mutation.\nThis makes read-modify-write logic straightforward:\n\n```ts\nconst todo = yield * ctx.db.get(\"todos\", args.id);\nyield * ctx.db.patch(\"todos\", todo.id, { completed: !todo.completed });\n```\n\nSee [Write to the database](writing-data.md) for the write methods and transaction behavior.\n"],
|
|
33635
|
-
["schema.md", "# Schema syntax\n\nDefine the data model in `server/schema.ts`. The keys returned from `defineSchema` are table\nnames, and each `table` call defines that table's fields:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n users: table({\n name: values.string(),\n }),\n todos: table({\n text: values.string(),\n completed: values.boolean(),\n ownerId: values.optional(values.id(\"users\")),\n }).index(\"by_completed\", [\"completed\"]),\n}));\n```\n\n## Field values\n\nRead [values](values.md) for the complete validator list and the TypeScript type produced by each\none. `values.id` only accepts a table declared in the same schema. Arrays and objects can be nested,\nand their contents can use any value validator.\n\n## System fields\n\nIgnotum adds three fields to every stored document:\n\n| Field | Type |\n| ----------- | ------------------------- |\n| `id` | The ID type for its table |\n| `createdAt` | `Date` |\n| `updatedAt` | `Date` |\n\nDo not declare these fields in a table. Do not pass them to `insert`, `patch`, or `replace`.\n\n## Indexes\n\nChain `index` after a table to declare an ordered index. The first argument is its name; the second\nis the field order:\n\n```ts\nmessages: table({\n channel: values.string(),\n authorId: values.id(\"users\"),\n text: values.string(),\n})\n .index(\"by_channel\", [\"channel\"])\n .index(\"by_channel_author\", [\"channel\", \"authorId\"]);\n```\n\nIndex fields must be required booleans, dates, IDs, integers, numbers, strings, or literals. Field\norder matters: `by_channel_author` can efficiently select one channel, or one channel and author,\nbut it is not an author-only index.\n\nEvery index uses `createdAt` and `id` as its final ordering fields. Read\n[Read from the database](reading-data.md) for selecting indexes, building ranges, ordering results,\nand choosing a result method.\n\n## Generated types\n\n`_generated/types.ts` exports the data model, document, and ID types:\n\n```ts\nimport type { DataModel, Doc, Id } from \"@/_generated/types.js\";\n\ntype Todo = Doc<\"todos\">;\ntype TodoId = Id<\"todos\">;\n```\n\n`Doc<\"todos\">` includes the fields from the `todos` table and its three system fields. An\n`Id<\"todos\">` cannot be passed where an `Id<\"users\">` is required.\n"],
|
|
33636
|
-
["server-functions.md", "# Server functions\n\nPut queries and mutations in `.ts` files directly inside `server`. The file name becomes the API\nmodule, and each exported function keeps its export name:\n\n```text\nserver/todos.ts -> api.todos.list\nserver/users.ts -> api.users.get\n```\n\nIgnotum ignores `schema.ts`, `index.ts`, test files, and names beginning with `_`.\n\nImport schema-bound builders from the generated server file. Import `Result` only when the module\nintroduces or catches typed errors:\n\n```ts\nimport { Result } from \"ignotum/server\";\n\nimport { mutation, query, values } from \"@/_generated/server.js\";\n```\n\nKeep the `.js` suffix on generated imports.\n\n## Define a function\n\nA function has optional argument, return, and public error schemas, plus a generator handler:\n\n```ts\nexport const getTitle = query({\n args: {\n id: values.id(\"todos\"),\n },\n returns: values.string(),\n\n handler: function* (ctx, args) {\n const todo = yield* ctx.db.get(\"todos\", args.id);\n return todo.text;\n },\n});\n```\n\nOmit `args` when the function takes no arguments. Omit `returns` when it returns nothing. An\nomitted `returns` only permits a `void` handler; Ignotum never infers an unchecked return schema.\n`yield*` waits for an Ignotum operation and propagates its typed application errors. Return\nsuccessful values with ordinary `return`.\n\nIgnotum validates arguments before running the handler. It also validates returned values and\npublic application errors before sending them to a client.\n\nAn argument-free query and a mutation with no return value can stay small:\n\n```ts\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n returns: values.array(Todo),\n\n handler: function* (ctx) {\n return yield* ctx.db.query(\"todos\").collect();\n },\n});\n\nexport const remove = mutation({\n args: { id: values.id(\"todos\") },\n\n handler: function* (ctx, args) {\n yield* ctx.db.delete(\"todos\", args.id);\n },\n});\n```\n\nCall an argument-free query as `useQuery(api.todos.list)`. An argument-free mutation returns a\nzero-argument function:\n\n```ts\nconst clear = useMutation(api.todos.clear);\nvoid clear();\n```\n\n## Database access\n\nQuery handlers receive the read API. Mutation handlers receive the same reads plus writes, and all\nof a mutation's writes commit together.\n\n- [Read from the database](reading-data.md) covers `find`, `get`, table and index queries, ranges,\n ordering, `collect`, `take`, `first`, and `unique`.\n- [Write to the database](writing-data.md) covers `insert`, `patch`, `replace`, `delete`, and\n mutation behavior.\n\nHosted apps limit document size, collection reads, function runtime, and stored app data. See\n[Limits](limits.md) for the current values.\n\n## Application errors\n\nDefine an application error with `values.error`. Its name becomes `_tag`:\n\n```ts\nconst TodoNotFound = values.error(\"TodoNotFound\", {\n id: values.id(\"todos\"),\n});\n```\n\n`Result.fail` deliberately stops the operation with a typed error:\n\n```ts\nyield * Result.fail(TodoNotFound({ id: args.id }));\n```\n\nThe `errors` field is optional. If omitted, Ignotum infers the handler's remaining application\nerrors. If supplied, it is the public contract and the handler must conform to it:\n\n```ts\nexport const toggle = mutation({\n args: {\n id: values.id(\"todos\"),\n },\n returns: values.boolean(),\n errors: TodoNotFound,\n\n handler: function* (ctx, args) {\n const todo = yield* ctx.db.get(\"todos\", args.id).catch({\n DocumentNotFound: (error) => Result.fail(TodoNotFound({ id: error.id })),\n });\n\n const completed = !todo.completed;\n yield* ctx.db.patch(\"todos\", args.id, { completed });\n return completed;\n },\n});\n```\n\nCombine public errors with `values.union`:\n\n```ts\nerrors: values.union(InvalidTodoText, TodoLimitReached),\n```\n\n## Catch and recover\n\nEvery Result operation has a partial, tag-based `catch`. Handlers receive the narrowed error type.\nUnmatched errors continue through the channel:\n\n```ts\nconst settings =\n yield *\n loadSettings().catch({\n SettingsNotFound: () => defaultSettings,\n });\n```\n\nReturn a plain value to recover. Return `Result.fail(...)` to map one error to another. Unknown tag\nnames fail the TypeScript check.\n\nUse `Result.try` for one catch boundary around several operations:\n\n```ts\nconst author =\n yield *\n Result.try(function* () {\n const membership = yield* ctx.db.get(\"memberships\", topic.membershipId);\n return yield* ctx.db.get(\"users\", membership.userId);\n }).catch({\n DocumentNotFound: () => Result.fail(InvalidAuthor({ topicId: topic.id })),\n });\n```\n\nThere is no async variant. Ignotum operations always use `yield*` in server code.\n\n## Standalone results\n\n`Result.succeed` remains useful for helpers that return a Result:\n\n```ts\nfunction validateName(name: string) {\n if (name.length === 0) {\n return Result.fail(InvalidName({}));\n }\n\n return Result.succeed(name.trim());\n}\n```\n\nA handler can use the helper with `const name = yield* validateName(args.name)`. Normal handlers do\nnot wrap successful returns in `Result.succeed`.\n\n## Internal failures and defects\n\nDatabase outages, internal runtime failures, and thrown JavaScript exceptions are not application\nerrors. Ignotum logs their full cause and sends only:\n\n```ts\n{\n _tag: \"InternalServerError\",\n requestId: \"...\",\n}\n```\n\nEvery generated client function includes `InternalServerError` in its Result error union. The\nrequest ID links the client-visible failure to server logs without exposing private details.\n`InternalServerError` is reserved by Ignotum: never define it with `values.error` or include it in a\nfunction's `errors` schema.\n"],
|
|
33637
|
-
["values.md", "# Values\n\nUse `values` validators to describe table fields, function arguments, return values, and application\nerrors. Each validator checks values at runtime and supplies the matching TypeScript type.\n\n| Validator | TypeScript type | Notes |\n| ----------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- |\n| `values.string()` | `string` | |\n| `values.number()` | `number` | Accepts finite JavaScript numbers, including integers. |\n| `values.integer()` | `number` | Accepts safe integers. |\n| `values.boolean()` | `boolean` | |\n| `values.date()` | `Date` | Accepts valid JavaScript dates. |\n| `values.null()` | `null` | Accepts only `null`. |\n| `values.literal(value)` | The exact type of `value` | Accepts one string, finite number, or boolean value. |\n| `values.literals(first, second, ...rest)` | A union of the supplied literal types | Requires at least two string, finite number, or boolean values. |\n| `values.id(\"todos\")` | `Id<\"todos\">` | Accepts an ID for a table declared in the same schema. IDs for different tables are different types. |\n| `values.doc(\"todos\")` | `Doc<\"todos\">` | Accepts a complete document, including its `id`, `createdAt`, and `updatedAt` system fields. |\n| `values.pagination()` | Pagination options | Defines the `pagination` argument used by `.paginate(...)`. |\n| `values.page(value)` | A page of `T` | Defines the return value of a paginated query. |\n| `values.optional(value)` | `T \\| undefined` | Makes an object or table field optional. The field may be omitted. |\n| `values.nullable(value)` | `T \\| null` | The value remains required unless it is also wrapped with `optional`. |\n| `values.array(value)` | `ReadonlyArray<T>` | Every item must match `value`. |\n| `values.object(fields)` | An object matching `fields` | Defines an object with known field names. |\n| `values.record(value)` | `Readonly<Record<string, T>>` | Defines an object with dynamic string keys whose values all match `value`. |\n| `values.union(...values)` | A union of the supplied types | Accepts a value matching any supplied validator. |\n| `values.never()` | `never` | No value can pass this validator. |\n| `values.error(\"Name\", fields)` | A tagged error object | Defines an application error whose `_tag` is the supplied name. |\n\n`T` means the TypeScript type produced by the wrapped validator.\n\n`values.doc`, `values.pagination`, and `values.page` are available on the schema-bound `values`\nexported by `_generated/server.ts`. They are not available while defining `server/schema.ts`.\n\n## Object transforms\n\nValidators created by `values.object(fields)` and `values.doc(\"table\")` support these chainable\nmethods:\n\n| Method | Result |\n| ------------------- | -------------------------------------------------------------------- |\n| `.pick(...keys)` | Keeps the listed fields. Every key must exist. |\n| `.omit(...keys)` | Removes the listed fields. Every key must exist. |\n| `.extend(fields)` | Adds fields. It rejects names that already exist. |\n| `.override(fields)` | Replaces validators for existing fields. It rejects new field names. |\n| `.partial()` | Makes every current field optional. |\n\nEach method works from the result of the previous method. Added fields can be picked, omitted, or\noverridden immediately. An omitted name can be added again with a different validator.\n\n```ts\nconst TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial().extend({\n requestId: values.string(),\n});\n```\n\nUse `override` when changing an existing field. This makes replacements visible in the definition\nand prevents `extend` from silently weakening fields such as `id`:\n\n```ts\nconst EditableTodo = values.doc(\"todos\").override({\n title: values.optional(values.string()),\n});\n```\n\nThese methods are only available on fixed object validators. Arrays, records, unions, errors, and\nprimitive validators do not expose them. After transforming a document validator, its inferred type\nmatches the current fields in the chain rather than the complete document type.\n\n### Reuse embedded objects in the schema\n\nCreate a fixed object validator inside the `defineSchema` callback when several stored fields share\nan object shape. A transform can derive a stored variant without repeating its fields:\n\n```ts\ndefineSchema(({ table, values }) => {\n const Contact = values.object({\n email: values.string(),\n phone: values.string(),\n });\n\n return {\n users: table({\n contact: Contact,\n }),\n publicProfiles: table({\n contact: Contact.omit(\"phone\"),\n }),\n };\n});\n```\n\nThis pattern suits embedded objects stored by more than one table, including full and reduced\nversions of the same object. The base validator stays inside `defineSchema`, where `values.id` can\ncheck its table references against the completed schema.\n\n### Derive server function validators from documents\n\nUse the schema-bound `values` from `_generated/server.ts` when a function input or output follows a\nstored document. These validators know every table name and the complete document fields, including\n`id`, `createdAt`, and `updatedAt`.\n\nReusable server validators can live in an ignored server module such as `server/_validators.ts`:\n\n```ts\nimport { values } from \"@/_generated/server.js\";\n\nexport const TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial();\n\nexport const PublicTodo = values.doc(\"todos\").omit(\"updatedAt\");\n```\n\nImport these validators into queries and mutations that need the same contract. This works well for\npatch inputs and document projections. Do not import generated validators into `server/schema.ts`.\nDocument validators depend on the schema, so importing them while defining that schema would create\na cycle.\n\n## Examples\n\n```ts\nconst TodoStatus = values.literals(\"pending\", \"completed\");\n\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n returns: values.array(Todo),\n handler: function* (ctx) {\n return yield* ctx.db.query(\"todos\").collect();\n },\n});\n```\n\nUse the other validators to define reusable values that do not represent a stored document:\n\n```ts\nconst TodoInput = values.object({\n status: TodoStatus,\n scheduledAt: values.nullable(values.date()),\n scores: values.record(values.integer()),\n title: values.string(),\n});\n```\n\nUse `optional` when a field may be absent. Use `nullable` when a present field may contain `null`:\n\n```ts\nvalues.object({\n nickname: values.optional(values.string()),\n deletedAt: values.nullable(values.date()),\n});\n```\n"],
|
|
33638
|
-
["writing-data.md", "# Write to the database\n\nMutation handlers can insert, patch, replace, and delete documents through `ctx.db`. Query handlers\nonly receive the read API.\n\n## Insert\n\n`insert` creates a document and returns its table-specific ID:\n\n```ts\nconst id =\n yield *\n ctx.db.insert(\"todos\", {\n text: \"Learn Ignotum\",\n completed: false,\n });\n```\n\nSupply every required table field. Omit optional fields when they have no value. Ignotum supplies\n`id`, `createdAt`, and `updatedAt`; never pass those system fields yourself.\n\n## Patch\n\n`patch` changes only the supplied fields:\n\n```ts\nyield * ctx.db.patch(\"todos\", args.id, { completed: true });\n```\n\nAll other fields keep their current values. Patching a missing document has no effect.\n\n## Replace\n\n`replace` supplies a new complete set of application fields:\n\n```ts\nyield *\n ctx.db.replace(\"todos\", args.id, {\n text: \"Build an app\",\n completed: false,\n });\n```\n\nEvery required field must be present. The document keeps its `id` and `createdAt`, while\n`updatedAt` advances. Replacing a missing document has no effect.\n\n## Delete\n\n`delete` removes a document by ID:\n\n```ts\nyield * ctx.db.delete(\"todos\", args.id);\n```\n\nDeleting a missing document has no effect.\n\n## Mutation behavior\n\nA mutation sees its earlier writes, including through indexed reads. Ignotum commits all of the\nmutation's writes together after the handler succeeds. If the handler fails with an application\nerror or encounters an internal failure, none of its writes are committed.\n\n```ts\nexport const completeOldest = mutation({\n handler: function* (ctx) {\n const todo = yield* ctx.db\n .query(\"todos\")\n .index(\"by_completed\", (range) => range.eq(\"completed\", false))\n .first();\n\n if (todo !== undefined) {\n yield* ctx.db.patch(\"todos\", todo.id, { completed: true });\n }\n },\n});\n```\n\nRead [Read from the database](reading-data.md) for `find`, `get`, table queries, indexed ranges,\nordering, and result methods.\n"]
|
|
33639
|
-
].map(([name, content]) => ({
|
|
33640
|
-
content,
|
|
34318
|
+
...references.map(([name, content]) => ({
|
|
34319
|
+
content: withoutFrontmatter(content),
|
|
33641
34320
|
path: `.agents/skills/ignotum/references/${name}`
|
|
33642
34321
|
}))
|
|
33643
34322
|
];
|
|
@@ -33828,6 +34507,17 @@ Open <http://127.0.0.1:3210>.
|
|
|
33828
34507
|
The app generator creates \`_generated\`. The dev server checks those files before it starts
|
|
33829
34508
|
and updates them when the schema or server functions change.
|
|
33830
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
|
+
|
|
33831
34521
|
## App files
|
|
33832
34522
|
|
|
33833
34523
|
- \`server/schema.ts\` defines the database tables.
|
|
@@ -33837,6 +34527,7 @@ and updates them when the schema or server functions change.
|
|
|
33837
34527
|
- Ignotum loads Tailwind CSS automatically. Custom CSS files are ordinary client modules.
|
|
33838
34528
|
- \`shared/utils.ts\` contains code shared across the app.
|
|
33839
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.
|
|
33840
34531
|
|
|
33841
34532
|
Run the typechecker after a change:
|
|
33842
34533
|
|
|
@@ -33845,6 +34536,9 @@ npx ignotum codegen
|
|
|
33845
34536
|
npx tsc --noEmit
|
|
33846
34537
|
\`\`\`
|
|
33847
34538
|
|
|
34539
|
+
The [Ignotum documentation](https://docs.ignotum.cloud) covers schema values, indexes, queries,
|
|
34540
|
+
mutations, client hooks, deployment, guarantees, and hosted limits.
|
|
34541
|
+
|
|
33848
34542
|
## Claude Code
|
|
33849
34543
|
|
|
33850
34544
|
If you use Claude Code, rename \`AGENTS.md\` to \`CLAUDE.md\` and \`.agents\` to \`.claude\` so it
|