dimah-formv0.2.0

Database

Persistence is required. A SQL database is not.

dimahForm({ database }) needs a ResponseStore. @dimah-form/server has no ORM. @dimah-form/db is optional — install it only when drafts must survive a restart.

AdapterPackageWhen
memoryAdapter()@dimah-form/serverTests, local, Quickstart
db(formDb)@dimah-form/dbProduction (FumaDB + Drizzle, Prisma, or Kysely)
Customyour codeImplement ResponseStore

1. In-memory

No extra packages. Data lives in the process and is gone on restart.

lib/form.ts
import { dimahForm, memoryAdapter } from "@dimah-form/server";
import { forms } from "@/lib/forms";

export const form = dimahForm({
  database: memoryAdapter(),
  forms,
});

2. Production

Install @dimah-form/db and fumadb when you need SQL:

bash pnpm add @dimah-form/db fumadb

Schema

Copy these tables, or generate with the CLI and re-add the indexes (FumaDB generate does not emit them).

lib/db/schema.ts
import { defineRelations } from "drizzle-orm";
import {
  sqliteTable,
  text,
  blob,
  integer,
  foreignKey,
  index,
} from "drizzle-orm/sqlite-core";

export const questionnaire = sqliteTable(
  "questionnaire",
  {
    id: text("id", { length: 255 })
      .primaryKey()
      .notNull()
      .$defaultFn(() => crypto.randomUUID()),
    slug: text("slug", { length: 255 }).unique(),
    title: text("title").notNull(),
    definition: blob("definition", { mode: "json" }).notNull(),
    status: text("status").notNull(),
    createdAt: integer("created_at", { mode: "timestamp" })
      .notNull()
      .defaultNow(),
    updatedAt: integer("updated_at", { mode: "timestamp" })
      .notNull()
      .defaultNow(),
  },
  (table) => [
    index("questionnaire_status_updated_at_idx").on(
      table.status,
      table.updatedAt,
    ),
  ],
);

export const response = sqliteTable(
  "response",
  {
    id: text("id", { length: 255 })
      .primaryKey()
      .notNull()
      .$defaultFn(() => crypto.randomUUID()),
    questionnaireId: text("questionnaire_id", { length: 255 }).notNull(),
    status: text("status").notNull(),
    definition: blob("definition", { mode: "json" }).notNull(),
    answers: blob("answers", { mode: "json" }).notNull(),
    respondentId: text("respondent_id", { length: 255 }),
    submittedAt: integer("submitted_at", { mode: "timestamp" }),
    createdAt: integer("created_at", { mode: "timestamp" })
      .notNull()
      .defaultNow(),
    updatedAt: integer("updated_at", { mode: "timestamp" })
      .notNull()
      .defaultNow(),
  },
  (table) => [
    foreignKey({
      columns: [table.questionnaireId],
      foreignColumns: [questionnaire.id],
      name: "response_questionnaire_questionnaire_fk",
    })
      .onUpdate("restrict")
      .onDelete("restrict"),
    index("response_questionnaire_id_updated_at_idx").on(
      table.questionnaireId,
      table.updatedAt,
    ),
    index("response_respondent_id_updated_at_idx").on(
      table.respondentId,
      table.updatedAt,
    ),
    index("response_draft_lookup_idx").on(
      table.questionnaireId,
      table.respondentId,
      table.status,
      table.updatedAt,
    ),
  ],
);

export const private_dimah_form_settings = sqliteTable(
  "private_dimah_form_settings",
  {
    id: text("id", { length: 255 }).primaryKey().notNull(),
    version: text("version", { length: 255 }).notNull().default("1.0.0"),
  },
);

export const relations = defineRelations(
  { questionnaire, response, private_dimah_form_settings },
  (r) => ({
    questionnaire: {
      responses: r.many.response({
        alias: "response_questionnaire",
      }),
    },
    response: {
      questionnaire: r.one.questionnaire({
        from: r.response.questionnaireId,
        to: r.questionnaire.id,
        alias: "response_questionnaire",
      }),
    },
  }),
);

Client

lib/db.ts
import { createClient } from "@libsql/client";
import { DimahFormDB } from "@dimah-form/db";
import { drizzle } from "drizzle-orm/libsql";
import { drizzleAdapter } from "fumadb/adapters/drizzle";
import { relations } from "./schema";

const sqlite = createClient({
  url: process.env.DATABASE_URL ?? "file:local.db",
});

export const formDb = DimahFormDB.client(
  drizzleAdapter({
    db: drizzle({ client: sqlite, relations }),
    provider: "sqlite",
  }),
);

Connect

lib/form.ts
import { db } from "@dimah-form/db";
import { dimahForm } from "@dimah-form/server";
import { formDb } from "@/lib/db";
import { forms } from "@/lib/forms";

export const form = dimahForm({
  database: db(formDb),
  forms,
});

export type Form = typeof form;

CLI

scripts/db-cli.mts
import { DimahFormDB } from "@dimah-form/db";
import { runCli } from "@dimah-form/db/cli";
import { drizzleAdapter } from "fumadb/adapters/drizzle";

void runCli(
  DimahFormDB.client(drizzleAdapter({ db: {} as never, provider: "sqlite" })),
);
node --import tsx scripts/db-cli.mts generate latest -o ./lib/schema.ts

generate overwrites the file. Re-add these indexes afterward:

IndexColumns
questionnaire_status_updated_at_idxstatus, updated_at
response_questionnaire_id_updated_at_idxquestionnaire_id, updated_at
response_respondent_id_updated_at_idxrespondent_id, updated_at
response_draft_lookup_idxquestionnaire_id, respondent_id, status, updated_at

Custom store

Implement ResponseStore and pass it to dimahForm({ database }) directly. Do not wrap it in db().


Next Steps

On this page