feat: deno-first storage package with sqlite host and graph schemas

Scaffolded @alkdev/storage from @ade/storage_sqlite and @ade/core/graphs:
- graphs/ module: TypeBox schema types + SchemaBuilder (from @ade/core/graphs)
- sqlite/ module: Drizzle table defs, relations, injectable client (from @ade/storage_sqlite)
- pg/ module: placeholder for Postgres host
- deno.json configured for JSR with subpath exports (./graphs, ./sqlite, ./pg)
- Imports swapped: @sinclair/typebox → @alkdev/typebox, drizzle-typebox → @alkdev/drizzlebox
- Client is now injectable (no hardcoded env vars or module-level side effects)
- no-slow-types lint excluded (Drizzle generics); --allow-slow-types on publish
This commit is contained in:
2026-05-28 12:19:48 +00:00
parent c6ea6c15e9
commit 8c68dd6b07
20 changed files with 540 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
deno.lock
.npmrc

33
deno.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "@alkdev/storage",
"version": "0.1.0",
"license": "MIT",
"exports": {
".": "./mod.ts",
"./graphs": "./src/graphs/mod.ts",
"./sqlite": "./src/sqlite/mod.ts",
"./pg": "./src/pg/mod.ts"
},
"imports": {
"@alkdev/typebox": "npm:@alkdev/typebox",
"@alkdev/drizzlebox": "npm:@alkdev/drizzlebox",
"drizzle-orm": "npm:drizzle-orm",
"drizzle-orm/sqlite-core": "npm:drizzle-orm/sqlite-core",
"drizzle-orm/pg-core": "npm:drizzle-orm/pg-core",
"@libsql/client": "npm:@libsql/client",
"postgres": "npm:postgres",
"@std/assert": "jsr:@std/assert"
},
"lint": {
"rules": {
"exclude": ["no-slow-types"]
}
},
"tasks": {
"check": "deno check mod.ts src/graphs/mod.ts src/sqlite/mod.ts",
"test": "deno test --allow-all test/",
"lint": "deno lint",
"fmt": "deno fmt",
"publish:dry": "deno publish --allow-slow-types --dry-run"
}
}

1
mod.ts Normal file
View File

@@ -0,0 +1 @@
export * from "./src/graphs/mod.ts";

2
src/graphs/mod.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./types.ts";
export * from "./schemaBuilder.ts";

View File

@@ -0,0 +1,63 @@
import { KindGuard, type TSchema } from "@alkdev/typebox";
import { Value } from "@alkdev/typebox/value";
import { assert } from "@std/assert";
import { GraphSchema, GraphConfig, NodeType, EdgeType } from "./types.ts";
export class SchemaBuilder {
private schema: {
config: Record<string, unknown>;
nodeTypes: Record<string, NodeType>;
edgeTypes: Record<string, EdgeType>;
} = {
config: {},
nodeTypes: {},
edgeTypes: {},
};
config(config: Partial<GraphConfig>): SchemaBuilder {
const configObj = Value.Default(GraphConfig, config) as GraphConfig;
this.check(GraphConfig, configObj);
this.schema.config = configObj as Record<string, unknown>;
return this;
}
nodeType(name: string, schema: TSchema): SchemaBuilder {
assert(KindGuard.IsSchema(schema), `type '${name}' is not a valid json schema.`);
if (!this.schema.nodeTypes) this.schema.nodeTypes = {};
const nodeTypeObj: NodeType = { name, schema };
this.check(NodeType, nodeTypeObj);
this.schema.nodeTypes[name] = nodeTypeObj;
return this;
}
edgeType(
name: string,
schema: TSchema,
options?: { allowedSourceTypes?: string[]; allowedTargetTypes?: string[] },
): SchemaBuilder {
assert(KindGuard.IsSchema(schema), `type '${name}' is not a valid json schema.`);
if (!this.schema.edgeTypes) this.schema.edgeTypes = {};
const edgeTypeObj: EdgeType = { name, schema, ...options };
this.check(EdgeType, edgeTypeObj);
this.schema.edgeTypes[name] = edgeTypeObj;
return this;
}
check(schema: TSchema, value: unknown): void {
if (!Value.Check(schema, value)) {
const errors = [...Value.Errors(schema, value)];
throw new Error(
`Invalid schema structure: ${JSON.stringify(errors.map((e) => `${e.path}: ${e.message}`))}`,
);
}
}
build(): GraphSchema {
this.check(GraphSchema, this.schema);
return this.schema as GraphSchema;
}
}

70
src/graphs/types.ts Normal file
View File

@@ -0,0 +1,70 @@
import { Type, type Static, type TSchema } from "@alkdev/typebox";
export const BaseNodeAttributes: TSchema = Type.Object({
created: Type.Optional(Type.String({ format: "date-time" })),
modified: Type.Optional(Type.String({ format: "date-time" })),
metadata: Type.Optional(Type.Record(Type.String(), Type.Any())),
});
export type BaseNodeAttributes = Static<typeof BaseNodeAttributes>;
export const BaseEdgeAttributes: TSchema = Type.Object({
type: Type.String(),
metadata: Type.Optional(Type.Record(Type.String(), Type.Any())),
});
export type BaseEdgeAttributes = Static<typeof BaseEdgeAttributes>;
export const GraphConfig: TSchema = Type.Object({
type: Type.Union([
Type.Literal("directed"),
Type.Literal("undirected"),
Type.Literal("mixed"),
], { default: "mixed" }),
multi: Type.Boolean({ default: true }),
allowSelfLoops: Type.Boolean({ default: true }),
});
export type GraphConfig = Static<typeof GraphConfig>;
export const NodeType: TSchema = Type.Object({
name: Type.String(),
schema: Type.Any(),
});
export type NodeType = Static<typeof NodeType>;
export const EdgeType: TSchema = Type.Object({
name: Type.String(),
schema: Type.Any(),
allowedSourceTypes: Type.Optional(Type.Array(Type.String())),
allowedTargetTypes: Type.Optional(Type.Array(Type.String())),
});
export type EdgeType = Static<typeof EdgeType>;
export const GraphSchema: TSchema = Type.Object({
config: GraphConfig,
nodeTypes: Type.Record(Type.String(), NodeType),
edgeTypes: Type.Record(Type.String(), EdgeType),
});
export type GraphSchema = Static<typeof GraphSchema>;
export enum EnumGraphStatus {
Active = "active",
Archived = "archived",
Draft = "draft",
}
export type GraphStatus = Static<typeof GraphStatus>;
export const GraphStatus: TSchema = Type.Enum(EnumGraphStatus);
export enum EnumGraphBaseType {
Directed = "directed",
Undirected = "undirected",
Mixed = "mixed",
}
export type GraphBaseType = Static<typeof GraphBaseType>;
export const GraphBaseType: TSchema = Type.Enum(EnumGraphBaseType);

3
src/pg/mod.ts Normal file
View File

@@ -0,0 +1,3 @@
// Postgres host — not yet implemented
// Will mirror sqlite/ structure with pgTable, jsonb(), timestamp(), pgEnum, etc.
// Import pattern: import { ... } from "@alkdev/storage/pg"

11
src/sqlite/client.ts Normal file
View File

@@ -0,0 +1,11 @@
import { drizzle, type LibSQLDatabase } from "drizzle-orm/libsql";
import type { Client } from "@libsql/client";
import * as schema from "./schema.ts";
export type SqliteSchema = typeof schema;
export type SqliteDatabase = LibSQLDatabase<SqliteSchema>;
export function createSqliteDatabase(client: Client): SqliteDatabase {
return drizzle(client, { schema }) as SqliteDatabase;
}

2
src/sqlite/mod.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./schema.ts";
export * from "./client.ts";

68
src/sqlite/relations.ts Normal file
View File

@@ -0,0 +1,68 @@
import { relations } from "drizzle-orm";
import {
graphTypes,
nodeTypes,
edgeTypes,
graphs,
nodes,
edges,
} from "./tables/index.ts";
export const graphTypeRelations = relations(graphTypes, ({ many }) => ({
nodeTypes: many(nodeTypes),
edgeTypes: many(edgeTypes),
graphs: many(graphs),
}));
export const nodeTypesRelations = relations(nodeTypes, ({ one }) => ({
graphType: one(graphTypes, {
fields: [nodeTypes.graphTypeId],
references: [graphTypes.id],
}),
}));
export const edgeTypesRelations = relations(edgeTypes, ({ one }) => ({
graphType: one(graphTypes, {
fields: [edgeTypes.graphTypeId],
references: [graphTypes.id],
}),
}));
export const graphsRelations = relations(graphs, ({ one, many }) => ({
graphType: one(graphTypes, {
fields: [graphs.graphTypeId],
references: [graphTypes.id],
}),
nodes: many(nodes),
edges: many(edges),
}));
export const edgesRelations = relations(edges, ({ one }) => ({
graph: one(graphs, {
fields: [edges.graphId],
references: [graphs.id],
}),
sourceNode: one(nodes, {
fields: [edges.graphId, edges.sourceNodeKey],
references: [nodes.graphId, nodes.key],
relationName: "sourceNode",
}),
targetNode: one(nodes, {
fields: [edges.graphId, edges.targetNodeKey],
references: [nodes.graphId, nodes.key],
relationName: "targetNode",
}),
}));
export const nodesRelations = relations(nodes, ({ one, many }) => ({
graph: one(graphs, {
fields: [nodes.graphId],
references: [graphs.id],
}),
outgoingEdges: many(edges, {
relationName: "sourceNode",
}),
incomingEdges: many(edges, {
relationName: "targetNode",
}),
}));

2
src/sqlite/schema.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./tables/index.ts";
export * from "./relations.ts";

View File

@@ -0,0 +1,30 @@
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
import { createInsertSchema, createSelectSchema } from "@alkdev/drizzlebox";
import { Type, type Static } from "@alkdev/typebox";
import { commonCols, ACTOR_TYPE } from "./common.ts";
export const actors = sqliteTable("actors", {
...commonCols,
name: text("name").notNull(),
type: text("type", { enum: ["human", "llm", "agent"] }).notNull(),
});
export const SelectActor = createSelectSchema(actors, {
metadata: Type.Object({}, { additionalProperties: true }),
createdAt: Type.Date(),
updatedAt: Type.Date(),
});
export type SelectActor = Static<typeof SelectActor>;
export const InsertActor = createInsertSchema(actors, {
name: Type.String({ minLength: 1, maxLength: 255 }),
type: Type.Union([
Type.Literal(ACTOR_TYPE.Human),
Type.Literal(ACTOR_TYPE.Llm),
Type.Literal(ACTOR_TYPE.Agent),
]),
metadata: Type.Optional(Type.Object({}, { additionalProperties: true })),
});
export type InsertActor = Static<typeof InsertActor>;

View File

@@ -0,0 +1,21 @@
import { sql } from "drizzle-orm";
import { text, integer } from "drizzle-orm/sqlite-core";
export const commonCols = {
id: text("id").primaryKey(),
metadata: text("metadata", { mode: "json" }).$type<Record<string, unknown>>().default({}),
createdAt: integer("created_at", { mode: "timestamp" })
.default(sql`(strftime('%s', 'now'))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" })
.default(sql`(strftime('%s', 'now'))`)
.notNull(),
};
export const ACTOR_TYPE = {
Human: "human",
Llm: "llm",
Agent: "agent",
} as const;
export type EnumValues<T extends Record<string, string>> = T[keyof T];

View File

@@ -0,0 +1,37 @@
import { sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
import { createInsertSchema, createSelectSchema } from "@alkdev/drizzlebox";
import { Type, type Static } from "@alkdev/typebox";
import { commonCols } from "./common.ts";
import { graphTypes } from "./graphTypes.ts";
export const edgeTypes = sqliteTable("edge_types", {
...commonCols,
graphTypeId: text("graph_type_id").notNull().references(() => graphTypes.id, { onDelete: "cascade" }),
name: text("name").notNull(),
description: text("description").default(""),
schema: text("schema", { mode: "json" }).$type<Record<string, unknown>>().notNull(),
allowedSourceTypes: text("allowed_source_types", { mode: "json" }).$type<string[]>().default([]),
allowedTargetTypes: text("allowed_target_types", { mode: "json" }).$type<string[]>().default([]),
}, (table) => ({
graphTypeNameIdx: unique().on(table.graphTypeId, table.name),
}));
export const SelectEdgeType = createSelectSchema(edgeTypes, {
schema: Type.Unknown(),
allowedSourceTypes: Type.Array(Type.String()),
allowedTargetTypes: Type.Array(Type.String()),
metadata: Type.Object({}, { additionalProperties: true }),
createdAt: Type.Date(),
updatedAt: Type.Date(),
});
export type SelectEdgeType = Static<typeof SelectEdgeType>;
export const InsertEdgeType = createInsertSchema(edgeTypes, {
name: Type.String({ minLength: 2 }),
schema: Type.Unknown(),
allowedSourceTypes: Type.Optional(Type.Array(Type.String())),
allowedTargetTypes: Type.Optional(Type.Array(Type.String())),
});
export type InsertEdgeType = Static<typeof InsertEdgeType>;

View File

@@ -0,0 +1,44 @@
import { sqliteTable, text, integer, unique, foreignKey } from "drizzle-orm/sqlite-core";
import { createInsertSchema, createSelectSchema } from "@alkdev/drizzlebox";
import { Type, type Static } from "@alkdev/typebox";
import { commonCols } from "./common.ts";
import { graphs } from "./graphs.ts";
import { nodes } from "./nodes.ts";
const AttributesSchema = Type.Record(Type.String(), Type.Any());
export const edges = sqliteTable("edges", {
...commonCols,
graphId: text("graph_id").notNull().references(() => graphs.id, { onDelete: "cascade" }),
key: text("key"),
sourceNodeKey: text("source_node_key").notNull(),
targetNodeKey: text("target_node_key").notNull(),
attributes: text("attributes", { mode: "json" }).notNull().default({}),
undirected: integer("undirected", { mode: "boolean" }).default(false),
}, (table) => ({
graphKeyIdx: unique().on(table.graphId, table.key),
sourceFk: foreignKey({
columns: [table.graphId, table.sourceNodeKey],
foreignColumns: [nodes.graphId, nodes.key],
}).onDelete("cascade"),
targetFk: foreignKey({
columns: [table.graphId, table.targetNodeKey],
foreignColumns: [nodes.graphId, nodes.key],
}).onDelete("cascade"),
}));
export const SelectEdge = createSelectSchema(edges, {
attributes: AttributesSchema,
metadata: Type.Object({}, { additionalProperties: true }),
createdAt: Type.Date(),
updatedAt: Type.Date(),
});
export type SelectEdge = Static<typeof SelectEdge>;
export const InsertEdge = createInsertSchema(edges, {
key: Type.String({ minLength: 1 }),
attributes: AttributesSchema,
});
export type InsertEdge = Static<typeof InsertEdge>;

View File

@@ -0,0 +1,38 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { createInsertSchema, createSelectSchema } from "@alkdev/drizzlebox";
import { Type, type Static } from "@alkdev/typebox";
import { commonCols } from "./common.ts";
const ConfigSchema = Type.Object({
type: Type.Union([
Type.Literal("directed"),
Type.Literal("undirected"),
Type.Literal("mixed"),
], { default: "mixed" }),
multi: Type.Boolean({ default: true }),
allowSelfLoops: Type.Boolean({ default: true }),
});
export const graphTypes = sqliteTable("graph_types", {
...commonCols,
name: text("name").notNull().unique(),
description: text("description").default(""),
config: text("config", { mode: "json" }).$type<Static<typeof ConfigSchema>>().notNull(),
version: integer("version").notNull().default(1),
});
export const SelectGraphType = createSelectSchema(graphTypes, {
metadata: Type.Object({}, { additionalProperties: true }),
createdAt: Type.Date(),
updatedAt: Type.Date(),
});
export type SelectGraphType = Static<typeof SelectGraphType>;
export const InsertGraphType = createInsertSchema(graphTypes, {
name: Type.String({ minLength: 2, maxLength: 255 }),
description: Type.Optional(Type.String()),
});
export type InsertGraphType = Static<typeof InsertGraphType>;

View File

@@ -0,0 +1,35 @@
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
import { createInsertSchema, createSelectSchema } from "@alkdev/drizzlebox";
import { Type, type Static } from "@alkdev/typebox";
import { commonCols } from "./common.ts";
import { graphTypes } from "./graphTypes.ts";
import { EnumGraphStatus } from "../../graphs/types.ts";
export const graphs = sqliteTable("graphs", {
...commonCols,
graphTypeId: text("graph_type_id").references(() => graphTypes.id, { onDelete: "set null" }),
name: text("name").notNull(),
description: text("description").default(""),
status: text("status", { enum: ["active", "archived", "draft"] })
.default("draft")
.notNull(),
});
export const SelectGraph = createSelectSchema(graphs, {
metadata: Type.Object({}, { additionalProperties: true }),
createdAt: Type.Date(),
updatedAt: Type.Date(),
});
export type SelectGraph = Static<typeof SelectGraph>;
export const InsertGraph = createInsertSchema(graphs, {
name: Type.String({ minLength: 2 }),
status: Type.Optional(Type.Union([
Type.Literal(EnumGraphStatus.Active),
Type.Literal(EnumGraphStatus.Archived),
Type.Literal(EnumGraphStatus.Draft),
])),
});
export type InsertGraph = Static<typeof InsertGraph>;

View File

@@ -0,0 +1,23 @@
export { graphs } from "./graphs.ts";
export type { SelectGraph, InsertGraph } from "./graphs.ts";
export { SelectGraph as SelectGraphSchema, InsertGraph as InsertGraphSchema } from "./graphs.ts";
export { nodes } from "./nodes.ts";
export type { InsertNode } from "./nodes.ts";
export { InsertNodeSchema } from "./nodes.ts";
export { edges } from "./edges.ts";
export type { SelectEdge, InsertEdge } from "./edges.ts";
export { SelectEdge as SelectEdgeSchema, InsertEdge as InsertEdgeSchema } from "./edges.ts";
export { graphTypes } from "./graphTypes.ts";
export type { SelectGraphType, InsertGraphType } from "./graphTypes.ts";
export { SelectGraphType as SelectGraphTypeSchema, InsertGraphType as InsertGraphTypeSchema } from "./graphTypes.ts";
export { nodeTypes } from "./nodeTypes.ts";
export type { SelectNodeType, InsertNodeType } from "./nodeTypes.ts";
export { SelectNodeType as SelectNodeTypeSchema, InsertNodeType as InsertNodeTypeSchema } from "./nodeTypes.ts";
export { edgeTypes } from "./edgeTypes.ts";
export type { SelectEdgeType, InsertEdgeType } from "./edgeTypes.ts";
export { SelectEdgeType as SelectEdgeTypeSchema, InsertEdgeType as InsertEdgeTypeSchema } from "./edgeTypes.ts";
export { actors } from "./actors.ts";
export type { SelectActor, InsertActor } from "./actors.ts";
export { SelectActor as SelectActorSchema, InsertActor as InsertActorSchema } from "./actors.ts";
export { commonCols, ACTOR_TYPE } from "./common.ts";
export type { EnumValues } from "./common.ts";

View File

@@ -0,0 +1,31 @@
import { sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
import { createInsertSchema, createSelectSchema } from "@alkdev/drizzlebox";
import { Type, type Static } from "@alkdev/typebox";
import { commonCols } from "./common.ts";
import { graphTypes } from "./graphTypes.ts";
export const nodeTypes = sqliteTable("node_types", {
...commonCols,
graphTypeId: text("graph_type_id").notNull().references(() => graphTypes.id, { onDelete: "cascade" }),
name: text("name").notNull(),
description: text("description").default(""),
schema: text("schema", { mode: "json" }).$type<Record<string, unknown>>().notNull(),
}, (table) => ({
graphTypeNameIdx: unique().on(table.graphTypeId, table.name),
}));
export const SelectNodeType = createSelectSchema(nodeTypes, {
schema: Type.Unknown(),
metadata: Type.Object({}, { additionalProperties: true }),
createdAt: Type.Date(),
updatedAt: Type.Date(),
});
export type SelectNodeType = Static<typeof SelectNodeType>;
export const InsertNodeType = createInsertSchema(nodeTypes, {
name: Type.String({ minLength: 2 }),
schema: Type.Unknown(),
});
export type InsertNodeType = Static<typeof InsertNodeType>;

View File

@@ -0,0 +1,23 @@
import { sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
import { createInsertSchema } from "@alkdev/drizzlebox";
import { Type, type Static } from "@alkdev/typebox";
import { commonCols } from "./common.ts";
import { graphs } from "./graphs.ts";
const AttributesSchema = Type.Record(Type.String(), Type.Any());
export const nodes = sqliteTable("nodes", {
...commonCols,
graphId: text("graph_id").notNull().references(() => graphs.id, { onDelete: "cascade" }),
key: text("key").notNull(),
attributes: text("attributes", { mode: "json" }).notNull().default({}),
}, (table) => ({
graphKeyIdx: unique().on(table.graphId, table.key),
}));
export const InsertNodeSchema = createInsertSchema(nodes, {
key: Type.String({ minLength: 1 }),
attributes: AttributesSchema,
});
export type InsertNode = Static<typeof InsertNodeSchema>;