kindstore 0.0.0 → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Alec Larson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # kindstore
2
+
3
+ kindstore is a registry-driven document store for Bun and SQLite. You define
4
+ document kinds with Zod, declare which top-level fields are queryable, and keep
5
+ payload and schema migrations explicit.
6
+
7
+ Requires Bun at runtime because kindstore uses `bun:sqlite`.
8
+
9
+ ```sh
10
+ bun add kindstore zod
11
+ ```
12
+
13
+ ```ts
14
+ import { z } from "zod";
15
+ import { kind, kindstore } from "kindstore";
16
+
17
+ const Post = z.object({
18
+ authorId: z.string(),
19
+ slug: z.string(),
20
+ title: z.string(),
21
+ status: z.enum(["draft", "published"]),
22
+ updatedAt: z.number().int(),
23
+ });
24
+
25
+ const db = kindstore({
26
+ connection: { filename: ":memory:" },
27
+ posts: kind("pst", Post)
28
+ .updatedAt("updatedAt")
29
+ .index("authorId")
30
+ .index("slug")
31
+ .index("status")
32
+ .index("updatedAt", { type: "integer" }),
33
+ });
34
+
35
+ const id = db.posts.newId();
36
+
37
+ db.posts.put(id, {
38
+ authorId: "usr_1",
39
+ slug: "hello-kindstore",
40
+ title: "Hello, kindstore",
41
+ status: "published",
42
+ });
43
+
44
+ const publishedPosts = db.posts.findMany({
45
+ where: { status: "published" },
46
+ orderBy: { updatedAt: "desc" },
47
+ });
48
+
49
+ const firstPage = db.posts.findPage({
50
+ where: { status: "published" },
51
+ orderBy: { updatedAt: "desc" },
52
+ limit: 20,
53
+ });
54
+ ```
55
+
56
+ Next:
57
+
58
+ - Start with the intermediate guides in [docs/course/README.md](./docs/course/README.md)
59
+ - Read the maintainer-facing system docs in [docs/architecture-overview.md](./docs/architecture-overview.md)
@@ -0,0 +1,164 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { z } from "zod";
3
+
4
+ //#region src/types.d.ts
5
+ type SqliteTypeHint = "text" | "integer" | "real" | "numeric";
6
+ type IndexDirection = "asc" | "desc";
7
+ type TaggedId<Tag extends string> = `${Tag}_${string}` & {
8
+ readonly __kindstoreTag?: Tag;
9
+ };
10
+ type KindDefinitionBag = {
11
+ tag: string;
12
+ schema: z.ZodObject<any>;
13
+ indexed: string;
14
+ createdAt: string;
15
+ updatedAt: string;
16
+ version: number;
17
+ };
18
+ type KindValue<T extends KindDefinitionBag> = z.output<T["schema"]>;
19
+ type KindManagedTimestampField<T extends KindDefinitionBag> = Extract<T["createdAt"] | T["updatedAt"], keyof KindValue<T> & string>;
20
+ type KindInputValue<T extends KindDefinitionBag> = Omit<KindValue<T>, KindManagedTimestampField<T>> & Partial<Pick<KindValue<T>, KindManagedTimestampField<T>>>;
21
+ type KindId<T extends KindDefinitionBag> = TaggedId<T["tag"]>;
22
+ type KindIndexedField<T extends KindDefinitionBag> = Extract<T["indexed"], keyof KindValue<T> & string>;
23
+ type FilterOperators<T> = {
24
+ in?: readonly Exclude<T, undefined>[];
25
+ gt?: Exclude<T, undefined>;
26
+ gte?: Exclude<T, undefined>;
27
+ lt?: Exclude<T, undefined>;
28
+ lte?: Exclude<T, undefined>;
29
+ };
30
+ type WhereOperand<T> = Exclude<T, undefined> | null | FilterOperators<T>;
31
+ type KindWhere<T extends KindDefinitionBag> = Partial<{ [K in KindIndexedField<T>]: WhereOperand<KindValue<T>[K]> }>;
32
+ type KindOrderBy<T extends KindDefinitionBag> = Partial<Record<KindIndexedField<T>, IndexDirection>>;
33
+ type FindManyOptions<T extends KindDefinitionBag> = {
34
+ where?: KindWhere<T>;
35
+ orderBy?: KindOrderBy<T>;
36
+ limit?: number;
37
+ };
38
+ type KindPageCursor<T extends KindDefinitionBag> = string & {
39
+ readonly __kindstorePageCursor?: T["tag"];
40
+ };
41
+ type FindPageOptions<T extends KindDefinitionBag> = {
42
+ where?: KindWhere<T>;
43
+ orderBy: KindOrderBy<T>;
44
+ limit: number;
45
+ after?: KindPageCursor<T>;
46
+ };
47
+ type FindPageResult<T extends KindDefinitionBag> = {
48
+ items: KindValue<T>[];
49
+ next?: KindPageCursor<T>;
50
+ };
51
+ type PatchValue<T> = T extends object ? Partial<T> : never;
52
+ type KindMigrationContext = {
53
+ readonly now: number;
54
+ };
55
+ type KindMigration<T extends object> = (value: Partial<T> & Record<string, unknown>, context: KindMigrationContext) => T | Record<string, unknown>;
56
+ type MetadataDefinitionMap = Record<string, z.ZodTypeAny>;
57
+ interface SchemaMigrationPlanner {
58
+ rename(previousKindKey: string, nextKindKey: string): this;
59
+ drop(previousKindKey: string): this;
60
+ retag(kindKey: string, previousTag: string): this;
61
+ }
62
+ type SchemaDefinition = {
63
+ migrate(planner: SchemaMigrationPlanner): void;
64
+ };
65
+ type MetadataValue<T extends MetadataDefinitionMap, K extends keyof T & string> = z.output<T[K]>;
66
+ type KindRegistry = Record<string, KindDefinition<any>>;
67
+ type ConnectionConfig = {
68
+ filename: string;
69
+ options?: ConstructorParameters<typeof Database>[1];
70
+ };
71
+ //#endregion
72
+ //#region src/kind.d.ts
73
+ type MultiIndexFields<T extends KindDefinitionBag> = { [K in keyof KindValue<T> & string]?: IndexDirection };
74
+ type IndexDefinition = {
75
+ field: string;
76
+ type?: SqliteTypeHint;
77
+ single: boolean;
78
+ };
79
+ type MultiIndexDefinition = {
80
+ name: string;
81
+ fields: readonly [string, IndexDirection][];
82
+ };
83
+ declare class KindDefinition<T extends KindDefinitionBag> {
84
+ readonly tag: T["tag"];
85
+ readonly schema: T["schema"];
86
+ version: T["version"];
87
+ createdAtField?: T["createdAt"];
88
+ updatedAtField?: T["updatedAt"];
89
+ readonly indexes: Map<string, IndexDefinition>;
90
+ readonly multiIndexes: MultiIndexDefinition[];
91
+ migrations?: Record<number, KindMigration<KindValue<T>>>;
92
+ constructor(tag: T["tag"], schema: T["schema"], version: T["version"]);
93
+ index<TKey extends keyof KindValue<T> & string>(field: TKey, options?: {
94
+ type?: SqliteTypeHint;
95
+ }): KindDefinition<Omit<T, "indexed"> & {
96
+ indexed: T["indexed"] | TKey;
97
+ }>;
98
+ createdAt<TKey extends keyof KindValue<T> & string>(field: TKey): KindDefinition<Omit<T, "createdAt"> & {
99
+ createdAt: TKey;
100
+ }>;
101
+ updatedAt<TKey extends keyof KindValue<T> & string>(field: TKey): KindDefinition<Omit<T, "updatedAt"> & {
102
+ updatedAt: TKey;
103
+ }>;
104
+ multi<const TName extends string, const TFields extends MultiIndexFields<T> & Record<string, IndexDirection>>(name: TName, fields: TFields): KindDefinition<Omit<T, "indexed"> & {
105
+ indexed: T["indexed"] | (keyof TFields & string);
106
+ }>;
107
+ migrate<const TVersion extends number>(version: TVersion, steps: Record<number, KindMigration<KindValue<T>>>): KindDefinition<Omit<T, "version"> & {
108
+ version: TVersion;
109
+ }>;
110
+ }
111
+ declare function kind<const TTag extends string, const TSchema extends z.ZodObject<any>>(tag: TTag, schema: TSchema): KindDefinition<{
112
+ tag: TTag;
113
+ schema: TSchema;
114
+ indexed: never;
115
+ createdAt: never;
116
+ updatedAt: never;
117
+ version: 1;
118
+ }>;
119
+ //#endregion
120
+ //#region src/runtime.d.ts
121
+ type KindCollectionSurface<T extends KindDefinitionBag> = {
122
+ newId(): KindId<T>;
123
+ get(id: KindId<T>): KindValue<T> | undefined;
124
+ put(id: KindId<T>, value: KindInputValue<T>): KindValue<T>;
125
+ delete(id: KindId<T>): boolean;
126
+ update(id: KindId<T>, updater: PatchValue<KindInputValue<T>> | ((current: KindValue<T>) => KindInputValue<T>)): KindValue<T> | undefined;
127
+ first(options?: FindManyOptions<T>): KindValue<T> | undefined;
128
+ findMany(options?: FindManyOptions<T>): KindValue<T>[];
129
+ findPage(options: FindPageOptions<T>): FindPageResult<T>;
130
+ iterate(options?: FindManyOptions<T>): IterableIterator<KindValue<T>>;
131
+ };
132
+ type MetadataSurface<T extends MetadataDefinitionMap> = {
133
+ get<K extends keyof T & string>(key: K): MetadataValue<T, K> | undefined;
134
+ set<K extends keyof T & string>(key: K, value: MetadataValue<T, K>): MetadataValue<T, K>;
135
+ delete<K extends keyof T & string>(key: K): boolean;
136
+ update<K extends keyof T & string>(key: K, updater: (current: MetadataValue<T, K> | undefined) => MetadataValue<T, K>): MetadataValue<T, K>;
137
+ };
138
+ type KindStoreSurface<TKinds extends KindRegistry, TMetadata extends MetadataDefinitionMap> = {
139
+ readonly raw: Database;
140
+ readonly metadata: MetadataSurface<TMetadata>;
141
+ batch<TResult>(callback: () => TResult): TResult;
142
+ close(): void;
143
+ } & { [K in keyof TKinds]: TKinds[K] extends KindDefinition<infer TBag> ? KindCollectionSurface<TBag> : never };
144
+ type PublicKindCollection<T extends KindDefinitionBag> = KindCollectionSurface<T>;
145
+ type PublicMetadataCollection<T extends MetadataDefinitionMap> = MetadataSurface<T>;
146
+ type PublicKindstore<TKinds extends KindRegistry, TMetadata extends MetadataDefinitionMap> = KindStoreSurface<TKinds, TMetadata>;
147
+ //#endregion
148
+ //#region src/store.d.ts
149
+ type AnyStoreInput = {
150
+ connection: ConnectionConfig;
151
+ metadata?: MetadataDefinitionMap;
152
+ schema?: SchemaDefinition;
153
+ } & Record<string, unknown>;
154
+ type InferKinds<TInput extends AnyStoreInput> = { [K in keyof TInput as K extends "connection" | "metadata" | "schema" ? never : TInput[K] extends KindDefinition<any> ? K : never]: Extract<TInput[K], KindDefinition<any>> };
155
+ type InferMetadata<TInput extends AnyStoreInput> = TInput extends {
156
+ metadata: infer TMetadata extends MetadataDefinitionMap;
157
+ } ? TMetadata : {};
158
+ type KindCollection<T extends KindDefinitionBag> = PublicKindCollection<T>;
159
+ type MetadataCollection<T extends MetadataDefinitionMap> = PublicMetadataCollection<T>;
160
+ type Kindstore<TKinds extends KindRegistry, TMetadata extends MetadataDefinitionMap = {}> = PublicKindstore<TKinds, TMetadata>;
161
+ declare function kindstore<const TInput extends AnyStoreInput>(input: TInput): Kindstore<InferKinds<TInput>, InferMetadata<TInput>>;
162
+ type MetadataSchemas = Record<string, z.ZodTypeAny>;
163
+ //#endregion
164
+ export { type ConnectionConfig, type FilterOperators, type FindManyOptions, type FindPageOptions, type FindPageResult, type IndexDirection, type KindCollection, KindDefinition, type KindDefinitionBag, type KindId, type KindIndexedField, type KindInputValue, type KindMigration, type KindMigrationContext, type KindOrderBy, type KindPageCursor, type KindRegistry, type KindValue, type KindWhere, type Kindstore, type MetadataCollection, type MetadataDefinitionMap, type MetadataSchemas, type MetadataValue, type PatchValue, type SchemaDefinition, type SchemaMigrationPlanner, type SqliteTypeHint, type TaggedId, type WhereOperand, kind, kindstore };
package/dist/index.mjs ADDED
@@ -0,0 +1,910 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { monotonicFactory } from "ulid";
3
+ //#region src/kind.ts
4
+ var KindDefinition = class {
5
+ tag;
6
+ schema;
7
+ version;
8
+ createdAtField;
9
+ updatedAtField;
10
+ indexes = /* @__PURE__ */ new Map();
11
+ multiIndexes = [];
12
+ migrations;
13
+ constructor(tag, schema, version) {
14
+ this.tag = tag;
15
+ this.schema = schema;
16
+ this.version = version;
17
+ }
18
+ index(field, options = {}) {
19
+ const current = this.indexes.get(field);
20
+ this.indexes.set(field, {
21
+ field,
22
+ single: true,
23
+ type: options.type ?? current?.type
24
+ });
25
+ return this;
26
+ }
27
+ createdAt(field) {
28
+ if (field === this.updatedAtField) throw new Error(`Kind "${this.tag}" cannot use "${field}" for both createdAt and updatedAt.`);
29
+ this.createdAtField = field;
30
+ return this;
31
+ }
32
+ updatedAt(field) {
33
+ if (field === this.createdAtField) throw new Error(`Kind "${this.tag}" cannot use "${field}" for both createdAt and updatedAt.`);
34
+ this.updatedAtField = field;
35
+ return this;
36
+ }
37
+ multi(name, fields) {
38
+ const entries = Object.entries(fields);
39
+ if (!entries.length) throw new Error(`Multi-index "${name}" must include at least one field.`);
40
+ this.multiIndexes.push({
41
+ name,
42
+ fields: entries
43
+ });
44
+ return this;
45
+ }
46
+ migrate(version, steps) {
47
+ if (!Number.isInteger(version) || version < 1) throw new Error(`Kind "${this.tag}" version must be a positive integer.`);
48
+ this.version = version;
49
+ this.migrations = steps;
50
+ return this;
51
+ }
52
+ };
53
+ function kind(tag, schema) {
54
+ return new KindDefinition(tag, schema, 1);
55
+ }
56
+ //#endregion
57
+ //#region src/util.ts
58
+ function isFilterOperators(value) {
59
+ return !!value && typeof value === "object" && !Array.isArray(value) && ("in" in value || "gt" in value || "gte" in value || "lt" in value || "lte" in value);
60
+ }
61
+ function assertTaggedId(tag, id) {
62
+ if (!id.startsWith(`${tag}_`)) throw new Error(`Expected ID for tag "${tag}", received "${id}".`);
63
+ }
64
+ function parsePayload(payload) {
65
+ return JSON.parse(payload);
66
+ }
67
+ function columnExpression(type, field) {
68
+ const extract = `json_extract("payload", '${`$."${field.replaceAll("\"", "\"\"")}"`}')`;
69
+ return type === "text" ? extract : `CAST(${extract} AS ${type.toUpperCase()})`;
70
+ }
71
+ function indexName(table, suffix) {
72
+ return `idx_${table}_${suffix}`;
73
+ }
74
+ function sameColumns(left, right) {
75
+ return left.length === right.length && left.every((value, index) => value === right[index]);
76
+ }
77
+ function isRecord(value) {
78
+ return !!value && typeof value === "object" && !Array.isArray(value);
79
+ }
80
+ function isSqliteTypeHint(value) {
81
+ return value === "text" || value === "integer" || value === "real" || value === "numeric";
82
+ }
83
+ function quoteIdentifier(value) {
84
+ return `"${value.replaceAll("\"", "\"\"")}"`;
85
+ }
86
+ function quoteString(value) {
87
+ return `'${value.replaceAll("'", "''")}'`;
88
+ }
89
+ function snakeCase(value) {
90
+ let result = "";
91
+ for (let index = 0; index < value.length; index++) {
92
+ const char = value[index];
93
+ const lower = char.toLowerCase();
94
+ if (index && char !== lower && value[index - 1] !== "_") result += "_";
95
+ result += lower;
96
+ }
97
+ return result;
98
+ }
99
+ //#endregion
100
+ //#region src/runtime.ts
101
+ const KINDSTORE_FORMAT_VERSION = 2;
102
+ const INTERNAL_TABLE = "__kindstore_internal";
103
+ const APP_METADATA_TABLE = "__kindstore_app_metadata";
104
+ const LEGACY_KIND_VERSIONS_TABLE = "__kindstore_kind_versions";
105
+ const LEGACY_APP_METADATA_TABLE = "__kindstore_metadata";
106
+ const STORE_FORMAT_VERSION_KEY = "store_format_version";
107
+ const KIND_VERSIONS_KEY = "kind_versions";
108
+ const SCHEMA_SNAPSHOT_KEY = "schema_snapshot";
109
+ const RESERVED_STORE_KEYS = new Set([
110
+ "batch",
111
+ "close",
112
+ "connection",
113
+ "metadata",
114
+ "raw"
115
+ ]);
116
+ const RESERVED_COLUMN_NAMES = new Set(["id", "payload"]);
117
+ const nextUlid = monotonicFactory();
118
+ const PAGE_CURSOR_VERSION = 1;
119
+ function createStore(connection, kinds, metadataDefinitions, schemaDefinition) {
120
+ const database = new Database(connection.filename, connection.options);
121
+ try {
122
+ return new KindstoreRuntime(database, normalizeKinds(kinds), metadataDefinitions, normalizeSchemaPlan(schemaDefinition)).publicStore;
123
+ } catch (error) {
124
+ database.close();
125
+ throw error;
126
+ }
127
+ }
128
+ var KindstoreRuntime = class {
129
+ database;
130
+ kinds;
131
+ publicStore;
132
+ internal;
133
+ metadata;
134
+ schemaPlan;
135
+ constructor(database, kinds, metadataDefinitions, schemaPlan) {
136
+ this.database = database;
137
+ this.kinds = kinds;
138
+ this.schemaPlan = schemaPlan;
139
+ this.publicStore = this;
140
+ this.ensureInternalTable();
141
+ this.internal = new InternalMetadataRuntime(database);
142
+ this.bootstrap();
143
+ this.metadata = new MetadataRuntime(database, metadataDefinitions);
144
+ this.publicStore.raw = database;
145
+ this.publicStore.metadata = this.metadata;
146
+ for (const [key, definition] of kinds) this.publicStore[key] = new KindCollectionRuntime(database, definition);
147
+ }
148
+ batch(callback) {
149
+ return this.database.transaction(callback)();
150
+ }
151
+ close() {
152
+ this.database.close();
153
+ }
154
+ bootstrap() {
155
+ this.database.transaction(() => {
156
+ this.ensureStoreFormatVersion();
157
+ this.database.run(`CREATE TABLE IF NOT EXISTS ${quoteIdentifier(APP_METADATA_TABLE)} (
158
+ "key" TEXT PRIMARY KEY NOT NULL,
159
+ "payload" TEXT NOT NULL,
160
+ "created_at" INTEGER NOT NULL,
161
+ "updated_at" INTEGER NOT NULL
162
+ ) STRICT`);
163
+ const previousSnapshot = this.applySchemaMigrations(this.internal.getSchemaSnapshot());
164
+ for (const definition of this.kinds.values()) {
165
+ this.ensureKindTable(definition);
166
+ this.ensureGeneratedColumns(definition);
167
+ this.reconcileIndexes(definition, previousSnapshot?.kinds[definition.key]);
168
+ this.dropStaleGeneratedColumns(definition, previousSnapshot?.kinds[definition.key]);
169
+ this.migrateKind(definition);
170
+ }
171
+ this.internal.setSchemaSnapshot(this.createSchemaSnapshot());
172
+ })();
173
+ }
174
+ ensureInternalTable() {
175
+ this.database.run(`CREATE TABLE IF NOT EXISTS ${quoteIdentifier(INTERNAL_TABLE)} (
176
+ "key" TEXT PRIMARY KEY NOT NULL,
177
+ "payload" TEXT NOT NULL,
178
+ "updated_at" INTEGER NOT NULL
179
+ ) STRICT`);
180
+ }
181
+ ensureStoreFormatVersion() {
182
+ const version = this.internal.getNumber(STORE_FORMAT_VERSION_KEY);
183
+ if (version == null) {
184
+ if (this.hasExistingStoreArtifacts()) throw new Error("Store is missing the kindstore format version and cannot be opened safely.");
185
+ this.internal.set(STORE_FORMAT_VERSION_KEY, KINDSTORE_FORMAT_VERSION);
186
+ return;
187
+ }
188
+ if (!Number.isInteger(version) || version < 1) throw new Error(`Invalid kindstore format version "${version}".`);
189
+ if (version > KINDSTORE_FORMAT_VERSION) throw new Error(`Store format version ${version} is newer than supported version ${KINDSTORE_FORMAT_VERSION}.`);
190
+ if (version < KINDSTORE_FORMAT_VERSION) {
191
+ this.migrateStoreFormat(version);
192
+ this.internal.set(STORE_FORMAT_VERSION_KEY, KINDSTORE_FORMAT_VERSION);
193
+ }
194
+ }
195
+ migrateStoreFormat(version) {
196
+ for (let currentVersion = version; currentVersion < KINDSTORE_FORMAT_VERSION; currentVersion++) switch (currentVersion) {
197
+ case 1:
198
+ this.migrateStoreFormat1To2();
199
+ break;
200
+ default: throw new Error(`Store format version ${currentVersion} cannot be upgraded to ${KINDSTORE_FORMAT_VERSION}.`);
201
+ }
202
+ }
203
+ migrateStoreFormat1To2() {
204
+ const tables = /* @__PURE__ */ new Set();
205
+ const snapshot = this.internal.getSchemaSnapshot();
206
+ for (const kind of Object.values(snapshot?.kinds ?? {})) tables.add(kind.table);
207
+ for (const definition of this.kinds.values()) tables.add(definition.table);
208
+ for (const table of tables) {
209
+ if (!this.hasTable(table)) continue;
210
+ const columns = new Set(this.database.query(`PRAGMA table_xinfo(${quoteString(table)})`).all().map((column) => column.name));
211
+ if (columns.has("created_at")) this.database.run(`ALTER TABLE ${quoteIdentifier(table)} DROP COLUMN "created_at"`);
212
+ if (columns.has("updated_at")) this.database.run(`ALTER TABLE ${quoteIdentifier(table)} DROP COLUMN "updated_at"`);
213
+ }
214
+ }
215
+ hasExistingStoreArtifacts() {
216
+ if (this.hasTable(LEGACY_KIND_VERSIONS_TABLE) || this.hasTable(LEGACY_APP_METADATA_TABLE)) return true;
217
+ for (const definition of this.kinds.values()) if (this.hasTable(definition.table)) return true;
218
+ return this.internal.keys().length > 0;
219
+ }
220
+ hasTable(name) {
221
+ return !!this.database.query(`SELECT 1 AS "exists" FROM "sqlite_master" WHERE "type" = 'table' AND "name" = ?`).get(name);
222
+ }
223
+ applySchemaMigrations(previousSnapshot) {
224
+ if (!previousSnapshot) return previousSnapshot;
225
+ const resolvedKinds = {};
226
+ const consumedPrevious = /* @__PURE__ */ new Set();
227
+ const usedRetags = /* @__PURE__ */ new Set();
228
+ for (const [key, previous] of Object.entries(previousSnapshot.kinds)) if (this.kinds.has(key)) {
229
+ resolvedKinds[key] = previous;
230
+ consumedPrevious.add(key);
231
+ }
232
+ for (const [previousKey, nextKey] of this.schemaPlan.renames) {
233
+ const previous = previousSnapshot.kinds[previousKey];
234
+ if (!previous) throw new Error(`Schema migration rename references unknown previous kind "${previousKey}".`);
235
+ const next = this.kinds.get(nextKey);
236
+ if (!next) throw new Error(`Schema migration rename target "${nextKey}" is not in the current registry.`);
237
+ if (resolvedKinds[nextKey]) throw new Error(`Schema migration rename target "${nextKey}" is already matched to a previous kind.`);
238
+ this.renameKind(previousKey, previous, nextKey, next);
239
+ resolvedKinds[nextKey] = {
240
+ ...previous,
241
+ table: next.table
242
+ };
243
+ consumedPrevious.add(previousKey);
244
+ }
245
+ for (const [previousKey, previous] of Object.entries(previousSnapshot.kinds)) {
246
+ if (consumedPrevious.has(previousKey)) continue;
247
+ if (this.schemaPlan.drops.has(previousKey)) {
248
+ this.dropKind(previousKey, previous);
249
+ consumedPrevious.add(previousKey);
250
+ continue;
251
+ }
252
+ if (!this.kinds.has(previousKey)) throw new Error(`Previous kind "${previousKey}" is missing from the current registry and requires schema.migrate(...).`);
253
+ }
254
+ for (const [key, current] of this.kinds) {
255
+ const previous = resolvedKinds[key];
256
+ if (!previous) continue;
257
+ if (this.retagKindIfNeeded(key, previous, current)) usedRetags.add(key);
258
+ }
259
+ for (const key of this.schemaPlan.retags.keys()) if (!usedRetags.has(key)) throw new Error(`Schema migration retag for kind "${key}" did not match a changed current kind.`);
260
+ return {
261
+ kindstoreVersion: previousSnapshot.kindstoreVersion,
262
+ kinds: resolvedKinds
263
+ };
264
+ }
265
+ renameKind(previousKey, previous, nextKey, next) {
266
+ if (this.kinds.has(previousKey)) throw new Error(`Schema migration rename source "${previousKey}" still exists in the current registry.`);
267
+ if (!this.hasTable(previous.table)) throw new Error(`Schema migration rename source table "${previous.table}" does not exist.`);
268
+ if (previous.table !== next.table) {
269
+ if (this.hasTable(next.table)) throw new Error(`Schema migration rename target table "${next.table}" already exists.`);
270
+ this.database.run(`ALTER TABLE ${quoteIdentifier(previous.table)} RENAME TO ${quoteIdentifier(next.table)}`);
271
+ }
272
+ this.internal.moveKindVersion(previousKey, nextKey);
273
+ }
274
+ dropKind(previousKey, previous) {
275
+ if (this.kinds.has(previousKey)) throw new Error(`Schema migration drop source "${previousKey}" still exists in the current registry.`);
276
+ if (this.hasTable(previous.table)) this.database.run(`DROP TABLE IF EXISTS ${quoteIdentifier(previous.table)}`);
277
+ this.internal.deleteKindVersion(previousKey);
278
+ }
279
+ retagKindIfNeeded(key, previous, current) {
280
+ if (previous.tag === current.definition.tag) return false;
281
+ if (this.schemaPlan.retags.get(key) !== previous.tag) throw new Error(`Kind "${key}" changed tag from "${previous.tag}" to "${current.definition.tag}" and requires schema.migrate(...).`);
282
+ const updateIds = this.database.query(`UPDATE ${quoteIdentifier(current.table)} SET "id" = ? WHERE "id" = ?`);
283
+ for (const row of this.database.query(`SELECT "id" FROM ${quoteIdentifier(current.table)} ORDER BY "id" ASC`).iterate()) {
284
+ if (!row.id.startsWith(`${previous.tag}_`)) throw new Error(`Kind "${key}" cannot retag row "${row.id}" because it does not use the previous tag prefix "${previous.tag}_".`);
285
+ updateIds.run(`${current.definition.tag}_${row.id.slice(previous.tag.length + 1)}`, row.id);
286
+ }
287
+ return true;
288
+ }
289
+ ensureKindTable(definition) {
290
+ this.database.run(`CREATE TABLE IF NOT EXISTS ${quoteIdentifier(definition.table)} (
291
+ "id" TEXT PRIMARY KEY NOT NULL,
292
+ "payload" TEXT NOT NULL
293
+ ) STRICT`);
294
+ }
295
+ ensureGeneratedColumns(definition) {
296
+ const existing = new Set(this.database.query(`PRAGMA table_xinfo(${quoteString(definition.table)})`).all().map((column) => column.name));
297
+ for (const column of definition.columns.values()) {
298
+ if (existing.has(column.column)) continue;
299
+ this.database.run(`ALTER TABLE ${quoteIdentifier(definition.table)} ADD COLUMN ${quoteIdentifier(column.column)} ${column.type.toUpperCase()} GENERATED ALWAYS AS (${columnExpression(column.type, column.field)}) VIRTUAL`);
300
+ }
301
+ }
302
+ reconcileIndexes(definition, previous) {
303
+ const current = snapshotIndexes(definition);
304
+ if (previous) for (const index of Object.values(previous.indexes)) {
305
+ const next = current[index.sqliteName];
306
+ if (next && sameColumns(index.columns, next.columns)) continue;
307
+ this.database.run(`DROP INDEX IF EXISTS ${quoteIdentifier(index.sqliteName)}`);
308
+ }
309
+ for (const index of Object.values(current)) this.database.run(`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(index.sqliteName)} ON ${quoteIdentifier(definition.table)} (${index.columns.join(", ")})`);
310
+ }
311
+ dropStaleGeneratedColumns(definition, previous) {
312
+ if (!previous) return;
313
+ const currentColumns = new Set(Array.from(definition.columns.values(), (column) => column.column));
314
+ const existingColumns = new Set(this.database.query(`PRAGMA table_xinfo(${quoteString(definition.table)})`).all().map((column) => column.name));
315
+ for (const column of Object.values(previous.columns)) {
316
+ if (currentColumns.has(column.column) || !existingColumns.has(column.column)) continue;
317
+ this.database.run(`ALTER TABLE ${quoteIdentifier(definition.table)} DROP COLUMN ${quoteIdentifier(column.column)}`);
318
+ }
319
+ }
320
+ createSchemaSnapshot() {
321
+ const kinds = {};
322
+ for (const [key, definition] of this.kinds) kinds[key] = snapshotKind(definition);
323
+ return {
324
+ kindstoreVersion: KINDSTORE_FORMAT_VERSION,
325
+ kinds
326
+ };
327
+ }
328
+ migrateKind(definition) {
329
+ const version = this.internal.getKindVersion(definition.key);
330
+ if (version == null) {
331
+ this.internal.setKindVersion(definition.key, definition.definition.version);
332
+ return;
333
+ }
334
+ if (version > definition.definition.version) throw new Error(`Kind "${definition.key}" is at version ${version}, but the registry declares version ${definition.definition.version}.`);
335
+ if (version === definition.definition.version) return;
336
+ const migrations = definition.definition.migrations;
337
+ if (!migrations) throw new Error(`Kind "${definition.key}" requires migrations from version ${version} to ${definition.definition.version}, but none were declared.`);
338
+ const updateRow = this.database.query(`UPDATE ${quoteIdentifier(definition.table)} SET "payload" = ? WHERE "id" = ?`);
339
+ this.database.transaction(() => {
340
+ const now = Date.now();
341
+ const context = { now };
342
+ for (const row of this.database.query(`SELECT "id", "payload" FROM ${quoteIdentifier(definition.table)} ORDER BY "id" ASC`).iterate()) {
343
+ let value = parsePayload(row.payload);
344
+ for (let currentVersion = version; currentVersion < definition.definition.version; currentVersion++) {
345
+ const step = migrations[currentVersion];
346
+ if (!step) throw new Error(`Kind "${definition.key}" is missing migration step ${currentVersion} -> ${currentVersion + 1}.`);
347
+ value = step(value, context);
348
+ }
349
+ updateRow.run(JSON.stringify(this.applyManagedTimestamps(definition, value, parsePayload(row.payload), now, false)), row.id);
350
+ }
351
+ this.internal.setKindVersion(definition.key, definition.definition.version);
352
+ })();
353
+ }
354
+ applyManagedTimestamps(definition, value, current, now, insert) {
355
+ const next = { ...value };
356
+ if (definition.createdAtField) if (current && Object.hasOwn(current, definition.createdAtField)) next[definition.createdAtField] = current[definition.createdAtField];
357
+ else if (insert) next[definition.createdAtField] = now;
358
+ else delete next[definition.createdAtField];
359
+ if (definition.updatedAtField) next[definition.updatedAtField] = now;
360
+ return definition.definition.schema.parse(next);
361
+ }
362
+ };
363
+ var KindCollectionRuntime = class {
364
+ database;
365
+ definition;
366
+ getStatement;
367
+ putStatement;
368
+ deleteStatement;
369
+ updateStatement;
370
+ constructor(database, definition) {
371
+ this.database = database;
372
+ this.definition = definition;
373
+ this.getStatement = database.query(`SELECT "id", "payload" FROM ${quoteIdentifier(definition.table)} WHERE "id" = ?`);
374
+ this.putStatement = database.query(`INSERT INTO ${quoteIdentifier(definition.table)} ("id", "payload") VALUES (?, ?)
375
+ ON CONFLICT("id") DO UPDATE SET "payload" = excluded."payload"`);
376
+ this.deleteStatement = database.query(`DELETE FROM ${quoteIdentifier(definition.table)} WHERE "id" = ?`);
377
+ this.updateStatement = database.query(`UPDATE ${quoteIdentifier(definition.table)} SET "payload" = ? WHERE "id" = ?`);
378
+ }
379
+ newId() {
380
+ return `${this.definition.definition.tag}_${nextUlid()}`;
381
+ }
382
+ get(id) {
383
+ assertTaggedId(this.definition.definition.tag, id);
384
+ const row = this.getStatement.get(id);
385
+ return row ? this.parseRow(row) : void 0;
386
+ }
387
+ put(id, value) {
388
+ assertTaggedId(this.definition.definition.tag, id);
389
+ return this.database.transaction(() => {
390
+ const row = this.getStatement.get(id);
391
+ const parsed = this.applyManagedTimestamps(value, row, Date.now(), !row);
392
+ this.putStatement.run(id, JSON.stringify(parsed));
393
+ return parsed;
394
+ })();
395
+ }
396
+ delete(id) {
397
+ assertTaggedId(this.definition.definition.tag, id);
398
+ return this.deleteStatement.run(id).changes > 0;
399
+ }
400
+ update(id, updater) {
401
+ assertTaggedId(this.definition.definition.tag, id);
402
+ return this.database.transaction(() => {
403
+ const row = this.getStatement.get(id);
404
+ if (!row) return;
405
+ const current = this.parseRow(row);
406
+ const parsed = this.applyManagedTimestamps(typeof updater === "function" ? updater(current) : {
407
+ ...current,
408
+ ...updater
409
+ }, row, Date.now(), false);
410
+ this.updateStatement.run(JSON.stringify(parsed), id);
411
+ return parsed;
412
+ })();
413
+ }
414
+ first(options = {}) {
415
+ const compiled = this.compileSelect({
416
+ where: options.where,
417
+ orderBy: options.orderBy,
418
+ limit: 1
419
+ });
420
+ const rows = this.database.query(compiled.sql).all(...compiled.values);
421
+ return rows[0] ? this.parseRow(rows[0]) : void 0;
422
+ }
423
+ findMany(options = {}) {
424
+ return Array.from(this.iterate(options));
425
+ }
426
+ findPage(options) {
427
+ if (!Number.isInteger(options.limit) || options.limit < 1) throw new Error(`Query limit for kind "${this.definition.key}" must be a positive integer when using findPage().`);
428
+ const orderEntries = resolveOrderBy(this.definition.columns, options.orderBy);
429
+ if (!orderEntries.length) throw new Error(`findPage() for kind "${this.definition.key}" requires an explicit orderBy.`);
430
+ const where = mergeWhereClauses(compileWhere(this.definition.columns, options.where), compilePageAfter(this.definition, orderEntries, options.after));
431
+ const sql = `SELECT "id", "payload" FROM ${quoteIdentifier(this.definition.table)}${where.sql}${compileOrderBy(orderEntries, true)} LIMIT ?`;
432
+ const rows = this.database.query(sql).all(...where.values, options.limit + 1);
433
+ const pageRows = rows.slice(0, options.limit);
434
+ const items = pageRows.map((row) => this.parseRow(row));
435
+ if (rows.length <= options.limit || !pageRows.length) return { items };
436
+ return {
437
+ items,
438
+ next: encodePageCursor(this.definition, orderEntries, pageRows[pageRows.length - 1], items.at(-1))
439
+ };
440
+ }
441
+ *iterate(options = {}) {
442
+ const compiled = this.compileSelect(options);
443
+ for (const row of this.database.query(compiled.sql).iterate(...compiled.values)) yield this.parseRow(row);
444
+ }
445
+ parseRow(row) {
446
+ return this.definition.definition.schema.parse(parsePayload(row.payload));
447
+ }
448
+ applyManagedTimestamps(value, row, now, insert) {
449
+ const current = row ? parsePayload(row.payload) : void 0;
450
+ const next = { ...value };
451
+ if (this.definition.createdAtField) if (current && Object.hasOwn(current, this.definition.createdAtField)) next[this.definition.createdAtField] = current[this.definition.createdAtField];
452
+ else if (insert) next[this.definition.createdAtField] = now;
453
+ else delete next[this.definition.createdAtField];
454
+ if (this.definition.updatedAtField) next[this.definition.updatedAtField] = now;
455
+ return this.definition.definition.schema.parse(next);
456
+ }
457
+ compileSelect(options) {
458
+ if (options.limit != null && (!Number.isInteger(options.limit) || options.limit < 0)) throw new Error(`Query limit for kind "${this.definition.key}" must be a non-negative integer.`);
459
+ const where = compileWhere(this.definition.columns, options.where);
460
+ const orderBy = compileOrderBy(resolveOrderBy(this.definition.columns, options.orderBy));
461
+ return {
462
+ sql: `SELECT "id", "payload" FROM ${quoteIdentifier(this.definition.table)}${where.sql}${orderBy}${options.limit == null ? "" : " LIMIT ?"}`,
463
+ values: options.limit == null ? where.values : [...where.values, options.limit]
464
+ };
465
+ }
466
+ };
467
+ var MetadataRuntime = class {
468
+ database;
469
+ definitions;
470
+ getStatement;
471
+ setStatement;
472
+ deleteStatement;
473
+ constructor(database, definitions) {
474
+ this.database = database;
475
+ this.definitions = definitions;
476
+ this.getStatement = database.query(`SELECT "payload" FROM ${quoteIdentifier(APP_METADATA_TABLE)} WHERE "key" = ?`);
477
+ this.setStatement = database.query(`INSERT INTO ${quoteIdentifier(APP_METADATA_TABLE)} ("key", "payload", "created_at", "updated_at") VALUES (?, ?, ?, ?)
478
+ ON CONFLICT("key") DO UPDATE SET "payload" = excluded."payload", "updated_at" = excluded."updated_at"`);
479
+ this.deleteStatement = database.query(`DELETE FROM ${quoteIdentifier(APP_METADATA_TABLE)} WHERE "key" = ?`);
480
+ }
481
+ get(key) {
482
+ const schema = this.definitions[key];
483
+ if (!schema) throw new Error(`Metadata key "${key}" is not declared.`);
484
+ const row = this.getStatement.get(key);
485
+ return row ? schema.parse(parsePayload(row.payload)) : void 0;
486
+ }
487
+ set(key, value) {
488
+ const schema = this.definitions[key];
489
+ if (!schema) throw new Error(`Metadata key "${key}" is not declared.`);
490
+ const parsed = schema.parse(value);
491
+ const now = Date.now();
492
+ this.setStatement.run(key, JSON.stringify(parsed), now, now);
493
+ return parsed;
494
+ }
495
+ delete(key) {
496
+ if (!this.definitions[key]) throw new Error(`Metadata key "${key}" is not declared.`);
497
+ return this.deleteStatement.run(key).changes > 0;
498
+ }
499
+ update(key, updater) {
500
+ return this.database.transaction(() => this.set(key, updater(this.get(key))))();
501
+ }
502
+ };
503
+ var InternalMetadataRuntime = class {
504
+ database;
505
+ getStatement;
506
+ deleteStatement;
507
+ setStatement;
508
+ constructor(database) {
509
+ this.database = database;
510
+ this.getStatement = database.query(`SELECT "payload" FROM ${quoteIdentifier(INTERNAL_TABLE)} WHERE "key" = ?`);
511
+ this.deleteStatement = database.query(`DELETE FROM ${quoteIdentifier(INTERNAL_TABLE)} WHERE "key" = ?`);
512
+ this.setStatement = database.query(`INSERT INTO ${quoteIdentifier(INTERNAL_TABLE)} ("key", "payload", "updated_at") VALUES (?, ?, ?)
513
+ ON CONFLICT("key") DO UPDATE SET "payload" = excluded."payload", "updated_at" = excluded."updated_at"`);
514
+ }
515
+ get(key) {
516
+ const row = this.getStatement.get(key);
517
+ return row ? parsePayload(row.payload) : void 0;
518
+ }
519
+ getNumber(key) {
520
+ const value = this.get(key);
521
+ if (value == null) return;
522
+ if (typeof value !== "number") throw new Error(`Internal metadata key "${key}" must be a number.`);
523
+ return value;
524
+ }
525
+ set(key, value) {
526
+ this.setStatement.run(key, JSON.stringify(value), Date.now());
527
+ }
528
+ delete(key) {
529
+ this.deleteStatement.run(key);
530
+ }
531
+ keys() {
532
+ return this.database.query(`SELECT "key" FROM ${quoteIdentifier(INTERNAL_TABLE)} ORDER BY "key" ASC`).all().map((row) => row.key);
533
+ }
534
+ getKindVersion(kind) {
535
+ const version = this.getKindVersions()?.[kind];
536
+ return typeof version === "number" ? version : void 0;
537
+ }
538
+ setKindVersion(kind, version) {
539
+ const current = this.getKindVersions();
540
+ const next = current ? {
541
+ ...current,
542
+ [kind]: version
543
+ } : { [kind]: version };
544
+ this.set(KIND_VERSIONS_KEY, next);
545
+ }
546
+ deleteKindVersion(kind) {
547
+ const current = this.getKindVersions();
548
+ if (!current) return;
549
+ const next = { ...current };
550
+ delete next[kind];
551
+ if (Object.keys(next).length) {
552
+ this.set(KIND_VERSIONS_KEY, next);
553
+ return;
554
+ }
555
+ this.delete(KIND_VERSIONS_KEY);
556
+ }
557
+ moveKindVersion(previousKind, nextKind) {
558
+ const version = this.getKindVersion(previousKind);
559
+ if (version == null) return;
560
+ if (this.getKindVersion(nextKind) != null) throw new Error(`Schema migration cannot move kind version from "${previousKind}" to "${nextKind}" because the target already has a version entry.`);
561
+ this.setKindVersion(nextKind, version);
562
+ this.deleteKindVersion(previousKind);
563
+ }
564
+ getSchemaSnapshot() {
565
+ const snapshot = this.get(SCHEMA_SNAPSHOT_KEY);
566
+ if (snapshot == null) return;
567
+ if (!isRecord(snapshot) || !Number.isInteger(snapshot.kindstoreVersion) || !isRecord(snapshot.kinds)) throw new Error(`Internal metadata key "${SCHEMA_SNAPSHOT_KEY}" is malformed.`);
568
+ for (const [kindKey, kind] of Object.entries(snapshot.kinds)) {
569
+ if (!isRecord(kind) || typeof kind.tag !== "string" || typeof kind.table !== "string" || !Number.isInteger(kind.version) || !isRecord(kind.columns) || !isRecord(kind.indexes)) throw new Error(`Internal metadata key "${SCHEMA_SNAPSHOT_KEY}" has an invalid kind entry for "${kindKey}".`);
570
+ for (const [field, column] of Object.entries(kind.columns)) if (!isRecord(column) || column.field !== field || typeof column.column !== "string" || typeof column.type !== "string" || !isSqliteTypeHint(column.type) || typeof column.single !== "boolean") throw new Error(`Internal metadata key "${SCHEMA_SNAPSHOT_KEY}" has an invalid column entry for "${kindKey}.${field}".`);
571
+ for (const [indexName, index] of Object.entries(kind.indexes)) if (!isRecord(index) || index.sqliteName !== indexName || !Array.isArray(index.columns) || index.columns.some((column) => typeof column !== "string")) throw new Error(`Internal metadata key "${SCHEMA_SNAPSHOT_KEY}" has an invalid index entry for "${kindKey}.${indexName}".`);
572
+ }
573
+ return snapshot;
574
+ }
575
+ setSchemaSnapshot(snapshot) {
576
+ this.set(SCHEMA_SNAPSHOT_KEY, snapshot);
577
+ }
578
+ getKindVersions() {
579
+ const versions = this.get(KIND_VERSIONS_KEY);
580
+ if (versions == null) return;
581
+ if (!isRecord(versions)) throw new Error(`Internal metadata key "${KIND_VERSIONS_KEY}" is malformed.`);
582
+ for (const [kind, version] of Object.entries(versions)) if (typeof version !== "number" || !Number.isInteger(version) || version < 1) throw new Error(`Internal metadata key "${KIND_VERSIONS_KEY}" has an invalid version for "${kind}".`);
583
+ return versions;
584
+ }
585
+ };
586
+ function normalizeSchemaPlan(schemaDefinition) {
587
+ const plan = {
588
+ drops: /* @__PURE__ */ new Set(),
589
+ renames: /* @__PURE__ */ new Map(),
590
+ retags: /* @__PURE__ */ new Map()
591
+ };
592
+ if (!schemaDefinition) return plan;
593
+ schemaDefinition.migrate(new SchemaMigrationPlannerRuntime(plan));
594
+ return plan;
595
+ }
596
+ var SchemaMigrationPlannerRuntime = class {
597
+ plan;
598
+ constructor(plan) {
599
+ this.plan = plan;
600
+ }
601
+ rename(previousKindKey, nextKindKey) {
602
+ if (!previousKindKey || !nextKindKey) throw new Error("Schema migration rename keys must be non-empty.");
603
+ if (previousKindKey === nextKindKey) throw new Error(`Schema migration rename from "${previousKindKey}" to itself is not allowed.`);
604
+ if (this.plan.drops.has(previousKindKey) || this.plan.renames.has(previousKindKey)) throw new Error(`Schema migration already defines an operation for previous kind "${previousKindKey}".`);
605
+ for (const existingNextKey of this.plan.renames.values()) if (existingNextKey === nextKindKey) throw new Error(`Schema migration already maps a previous kind to "${nextKindKey}".`);
606
+ this.plan.renames.set(previousKindKey, nextKindKey);
607
+ return this;
608
+ }
609
+ drop(previousKindKey) {
610
+ if (!previousKindKey) throw new Error("Schema migration drop key must be non-empty.");
611
+ if (this.plan.renames.has(previousKindKey) || this.plan.drops.has(previousKindKey)) throw new Error(`Schema migration already defines an operation for previous kind "${previousKindKey}".`);
612
+ this.plan.drops.add(previousKindKey);
613
+ return this;
614
+ }
615
+ retag(kindKey, previousTag) {
616
+ if (!kindKey || !previousTag) throw new Error("Schema migration retag arguments must be non-empty.");
617
+ if (this.plan.retags.has(kindKey)) throw new Error(`Schema migration already defines a retag for kind "${kindKey}".`);
618
+ this.plan.retags.set(kindKey, previousTag);
619
+ return this;
620
+ }
621
+ };
622
+ function normalizeKinds(kinds) {
623
+ const definitions = /* @__PURE__ */ new Map();
624
+ const seenTags = /* @__PURE__ */ new Set();
625
+ const seenTables = /* @__PURE__ */ new Set();
626
+ for (const [key, value] of Object.entries(kinds)) {
627
+ if (!(value instanceof KindDefinition)) throw new Error(`Property "${key}" is not a kind definition.`);
628
+ if (RESERVED_STORE_KEYS.has(key)) throw new Error(`Kind key "${key}" is reserved.`);
629
+ if (seenTags.has(value.tag)) throw new Error(`Kind tag "${value.tag}" is declared more than once.`);
630
+ const table = snakeCase(key);
631
+ if (seenTables.has(table)) throw new Error(`Kind key "${key}" collides with an existing table name.`);
632
+ const shape = value.schema.shape;
633
+ if (value.createdAtField && value.createdAtField === value.updatedAtField) throw new Error(`Kind "${value.tag}" cannot use "${value.createdAtField}" for both createdAt and updatedAt.`);
634
+ validateManagedTimestampField(value, shape, value.createdAtField, "createdAt");
635
+ validateManagedTimestampField(value, shape, value.updatedAtField, "updatedAt");
636
+ seenTags.add(value.tag);
637
+ seenTables.add(table);
638
+ definitions.set(key, {
639
+ key,
640
+ table,
641
+ columns: normalizeColumns(value),
642
+ createdAtField: value.createdAtField,
643
+ updatedAtField: value.updatedAtField,
644
+ definition: value
645
+ });
646
+ }
647
+ return definitions;
648
+ }
649
+ function normalizeColumns(definition) {
650
+ const shape = definition.schema.shape;
651
+ const columns = /* @__PURE__ */ new Map();
652
+ const seenColumns = /* @__PURE__ */ new Set();
653
+ for (const index of definition.indexes.values()) {
654
+ assertTopLevelField(definition.tag, shape, index.field);
655
+ const column = columnName(index.field);
656
+ if (seenColumns.has(column)) throw new Error(`Kind "${definition.tag}" has multiple indexed fields that map to column "${column}".`);
657
+ seenColumns.add(column);
658
+ columns.set(index.field, {
659
+ field: index.field,
660
+ column,
661
+ single: index.single,
662
+ type: index.type ?? inferSqliteType(shape[index.field], definition.tag, index.field)
663
+ });
664
+ }
665
+ for (const multiIndex of definition.multiIndexes) for (const [field] of multiIndex.fields) {
666
+ if (columns.has(field)) continue;
667
+ assertTopLevelField(definition.tag, shape, field);
668
+ const column = columnName(field);
669
+ if (seenColumns.has(column)) throw new Error(`Kind "${definition.tag}" has multiple indexed fields that map to column "${column}".`);
670
+ seenColumns.add(column);
671
+ columns.set(field, {
672
+ field,
673
+ column,
674
+ single: false,
675
+ type: inferSqliteType(shape[field], definition.tag, field)
676
+ });
677
+ }
678
+ return columns;
679
+ }
680
+ function assertTopLevelField(tag, shape, field) {
681
+ if (!(field in shape)) throw new Error(`Kind "${tag}" references unknown field "${field}".`);
682
+ }
683
+ function validateManagedTimestampField(definition, shape, field, name) {
684
+ if (!field) return;
685
+ assertTopLevelField(definition.tag, shape, field);
686
+ if (inferSqliteType(shape[field], definition.tag, field) !== "integer") throw new Error(`Kind "${definition.tag}" ${name} field "${field}" must be an integer.`);
687
+ }
688
+ function inferSqliteType(schema, tag, field) {
689
+ while (schema?._def?.type === "optional" || schema?._def?.type === "nullable" || schema?._def?.type === "default" || schema?._def?.type === "readonly" || schema?._def?.type === "catch") schema = schema._def.innerType;
690
+ switch (schema?._def?.type) {
691
+ case "string":
692
+ case "enum": return "text";
693
+ case "boolean": return "integer";
694
+ case "literal": {
695
+ const values = schema._def.values;
696
+ const value = values instanceof Set ? values.values().next().value : values?.[0];
697
+ return typeof value === "number" || typeof value === "boolean" ? "integer" : "text";
698
+ }
699
+ case "number": return schema._def.checks?.some((check) => check.isInt) ? "integer" : "real";
700
+ default: throw new Error(`Kind "${tag}" field "${field}" needs an explicit SQLite type hint.`);
701
+ }
702
+ }
703
+ function compileWhere(columns, where) {
704
+ if (!where || !Object.keys(where).length) return {
705
+ sql: "",
706
+ values: []
707
+ };
708
+ const parts = [];
709
+ const values = [];
710
+ for (const [field, operand] of Object.entries(where)) {
711
+ const column = columns.get(field);
712
+ if (!column) throw new Error(`Field "${field}" is not indexed and cannot be queried.`);
713
+ if (isFilterOperators(operand)) {
714
+ if (operand.in) {
715
+ if (!operand.in.length) {
716
+ parts.push("0 = 1");
717
+ continue;
718
+ }
719
+ parts.push(`${quoteIdentifier(column.column)} IN (${operand.in.map(() => "?").join(", ")})`);
720
+ values.push(...operand.in);
721
+ }
722
+ if (operand.gt != null) {
723
+ parts.push(`${quoteIdentifier(column.column)} > ?`);
724
+ values.push(operand.gt);
725
+ }
726
+ if (operand.gte != null) {
727
+ parts.push(`${quoteIdentifier(column.column)} >= ?`);
728
+ values.push(operand.gte);
729
+ }
730
+ if (operand.lt != null) {
731
+ parts.push(`${quoteIdentifier(column.column)} < ?`);
732
+ values.push(operand.lt);
733
+ }
734
+ if (operand.lte != null) {
735
+ parts.push(`${quoteIdentifier(column.column)} <= ?`);
736
+ values.push(operand.lte);
737
+ }
738
+ continue;
739
+ }
740
+ if (operand == null) {
741
+ parts.push(`${quoteIdentifier(column.column)} IS NULL`);
742
+ continue;
743
+ }
744
+ parts.push(`${quoteIdentifier(column.column)} = ?`);
745
+ values.push(operand);
746
+ }
747
+ return {
748
+ sql: parts.length ? ` WHERE ${parts.join(" AND ")}` : "",
749
+ values
750
+ };
751
+ }
752
+ function mergeWhereClauses(...clauses) {
753
+ const parts = [];
754
+ const values = [];
755
+ for (const clause of clauses) {
756
+ if (!clause.sql) continue;
757
+ parts.push(clause.sql.replace(/^ WHERE /, ""));
758
+ values.push(...clause.values);
759
+ }
760
+ return {
761
+ sql: parts.length ? ` WHERE ${parts.join(" AND ")}` : "",
762
+ values
763
+ };
764
+ }
765
+ function resolveOrderBy(columns, orderBy) {
766
+ if (!orderBy || !Object.keys(orderBy).length) return [];
767
+ return Object.entries(orderBy).map(([field, direction]) => {
768
+ if (!direction) throw new Error(`Order direction for "${field}" is required.`);
769
+ const column = columns.get(field);
770
+ if (!column) throw new Error(`Field "${field}" is not indexed and cannot be ordered.`);
771
+ return {
772
+ field,
773
+ direction,
774
+ column
775
+ };
776
+ });
777
+ }
778
+ function tieBreakerDirection(orderBy) {
779
+ return orderBy[orderBy.length - 1].direction;
780
+ }
781
+ function compileOrderBy(orderBy, includeIdTieBreaker = false) {
782
+ if (!orderBy.length) return "";
783
+ const parts = orderBy.map(({ column, direction }) => `${quoteIdentifier(column.column)} ${direction.toUpperCase()}`);
784
+ if (includeIdTieBreaker) parts.push(`"id" ${tieBreakerDirection(orderBy).toUpperCase()}`);
785
+ return ` ORDER BY ${parts.join(", ")}`;
786
+ }
787
+ function compilePageAfter(definition, orderBy, after) {
788
+ if (!after) return {
789
+ sql: "",
790
+ values: []
791
+ };
792
+ const cursor = decodePageCursor(after);
793
+ if (cursor.version !== PAGE_CURSOR_VERSION) throw new Error(`Unsupported findPage() cursor version "${cursor.version}".`);
794
+ if (cursor.tag !== definition.definition.tag) throw new Error(`findPage() cursor does not belong to kind "${definition.key}".`);
795
+ if (cursor.order.length !== orderBy.length) throw new Error(`findPage() cursor does not match the requested orderBy for kind "${definition.key}".`);
796
+ for (const [index, [field, direction]] of cursor.order.entries()) {
797
+ const expected = orderBy[index];
798
+ if (field !== expected.field || direction !== expected.direction) throw new Error(`findPage() cursor does not match the requested orderBy for kind "${definition.key}".`);
799
+ }
800
+ if (cursor.values.length !== orderBy.length) throw new Error(`findPage() cursor is malformed for kind "${definition.key}".`);
801
+ const disjuncts = [];
802
+ const values = [];
803
+ for (let pivot = 0; pivot < orderBy.length; pivot += 1) {
804
+ const parts = [];
805
+ for (let index = 0; index < pivot; index += 1) {
806
+ const value = cursor.values[index];
807
+ if (value == null) throw new Error(`findPage() cursor cannot continue on nullish ordered field "${orderBy[index].field}".`);
808
+ parts.push(`${quoteIdentifier(orderBy[index].column.column)} = ?`);
809
+ values.push(value);
810
+ }
811
+ const pivotValue = cursor.values[pivot];
812
+ if (pivotValue == null) throw new Error(`findPage() cursor cannot continue on nullish ordered field "${orderBy[pivot].field}".`);
813
+ parts.push(`${quoteIdentifier(orderBy[pivot].column.column)} ${orderBy[pivot].direction === "asc" ? ">" : "<"} ?`);
814
+ values.push(pivotValue);
815
+ disjuncts.push(`(${parts.join(" AND ")})`);
816
+ }
817
+ const tieBreakerParts = orderBy.map(({ column }, index) => {
818
+ const value = cursor.values[index];
819
+ if (value == null) throw new Error(`findPage() cursor cannot continue on nullish ordered field "${orderBy[index].field}".`);
820
+ values.push(value);
821
+ return `${quoteIdentifier(column.column)} = ?`;
822
+ });
823
+ values.push(cursor.id);
824
+ tieBreakerParts.push(`"id" ${tieBreakerDirection(orderBy) === "asc" ? ">" : "<"} ?`);
825
+ disjuncts.push(`(${tieBreakerParts.join(" AND ")})`);
826
+ return {
827
+ sql: ` WHERE (${disjuncts.join(" OR ")})`,
828
+ values
829
+ };
830
+ }
831
+ function encodePageCursor(definition, orderBy, row, value) {
832
+ const values = orderBy.map(({ field }) => {
833
+ const fieldValue = value[field];
834
+ if (fieldValue == null) throw new Error(`findPage() cannot paginate on nullish ordered field "${field}".`);
835
+ return fieldValue;
836
+ });
837
+ return Buffer.from(JSON.stringify({
838
+ version: PAGE_CURSOR_VERSION,
839
+ tag: definition.definition.tag,
840
+ order: orderBy.map(({ field, direction }) => [field, direction]),
841
+ values,
842
+ id: row.id
843
+ })).toString("base64url");
844
+ }
845
+ function decodePageCursor(cursor) {
846
+ let parsed;
847
+ try {
848
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
849
+ } catch {
850
+ throw new Error("findPage() cursor is malformed.");
851
+ }
852
+ if (!isRecord(parsed)) throw new Error("findPage() cursor is malformed.");
853
+ const { version, tag, order, values, id } = parsed;
854
+ if (version !== PAGE_CURSOR_VERSION || typeof tag !== "string" || !Array.isArray(order) || !Array.isArray(values) || typeof id !== "string") throw new Error("findPage() cursor is malformed.");
855
+ for (const entry of order) if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || entry[1] !== "asc" && entry[1] !== "desc") throw new Error("findPage() cursor is malformed.");
856
+ return {
857
+ version,
858
+ tag,
859
+ order,
860
+ values,
861
+ id
862
+ };
863
+ }
864
+ function columnName(field) {
865
+ const column = snakeCase(field);
866
+ return RESERVED_COLUMN_NAMES.has(column) ? `doc_${column}` : column;
867
+ }
868
+ function snapshotKind(definition) {
869
+ const columns = {};
870
+ for (const column of definition.columns.values()) columns[column.field] = {
871
+ field: column.field,
872
+ column: column.column,
873
+ type: column.type,
874
+ single: column.single
875
+ };
876
+ return {
877
+ tag: definition.definition.tag,
878
+ table: definition.table,
879
+ version: definition.definition.version,
880
+ columns,
881
+ indexes: snapshotIndexes(definition)
882
+ };
883
+ }
884
+ function snapshotIndexes(definition) {
885
+ const indexes = {};
886
+ for (const column of definition.columns.values()) {
887
+ if (!column.single) continue;
888
+ const sqliteName = indexName(definition.table, column.column);
889
+ indexes[sqliteName] = {
890
+ sqliteName,
891
+ columns: [quoteIdentifier(column.column)]
892
+ };
893
+ }
894
+ for (const multiIndex of definition.definition.multiIndexes) {
895
+ const sqliteName = indexName(definition.table, snakeCase(multiIndex.name));
896
+ indexes[sqliteName] = {
897
+ sqliteName,
898
+ columns: multiIndex.fields.map(([field, direction]) => `${quoteIdentifier(definition.columns.get(field).column)} ${direction.toUpperCase()}`)
899
+ };
900
+ }
901
+ return indexes;
902
+ }
903
+ //#endregion
904
+ //#region src/store.ts
905
+ function kindstore(input) {
906
+ const { connection, metadata, schema, ...rest } = input;
907
+ return createStore(connection, rest, metadata ?? {}, schema);
908
+ }
909
+ //#endregion
910
+ export { KindDefinition, kind, kindstore };
package/package.json CHANGED
@@ -1,13 +1,53 @@
1
1
  {
2
2
  "name": "kindstore",
3
- "version": "0.0.0",
4
- "description": "",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "keywords": [],
10
- "author": "",
3
+ "version": "0.1.1",
4
+ "description": "A lightweight, typed document store on SQLite with Zod schemas, indexed queries, and explicit migrations.",
11
5
  "license": "MIT",
12
- "type": "commonjs"
13
- }
6
+ "author": "Alec Larson",
7
+ "homepage": "https://github.com/alloc/kindstore#readme",
8
+ "bugs": {
9
+ "url": "https://github.com/alloc/kindstore/issues"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/alloc/kindstore.git"
14
+ },
15
+ "keywords": [
16
+ "bun",
17
+ "sqlite",
18
+ "document-store",
19
+ "zod",
20
+ "migrations",
21
+ "typesafe"
22
+ ],
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "type": "module",
27
+ "exports": {
28
+ "types": "./dist/index.d.mts",
29
+ "import": "./dist/index.mjs"
30
+ },
31
+ "sideEffects": false,
32
+ "engines": {
33
+ "bun": ">=1.3.11"
34
+ },
35
+ "dependencies": {
36
+ "ulid": "^3.0.2",
37
+ "zod": "^4.3.6"
38
+ },
39
+ "devDependencies": {
40
+ "@types/bun": "^1.3.11",
41
+ "oxfmt": "^0.43.0",
42
+ "oxlint": "^1.58.0",
43
+ "tsdown": "^0.21.7",
44
+ "typescript": "^6.0.2"
45
+ },
46
+ "scripts": {
47
+ "dev": "tsdown --sourcemap --watch",
48
+ "build": "tsdown",
49
+ "format": "oxfmt .",
50
+ "lint": "oxlint src",
51
+ "test": "bun test"
52
+ }
53
+ }
package/readme.md DELETED
@@ -1 +0,0 @@
1
- Coming soon...