dimah-formv0.6.1
Server

Persistence

Store forms and responses in memory, SQL, or a custom ResponseStore.

database is required by dimahForm(). It is a first-class instance option, not a plugin. @dimah-form/server intentionally contains no ORM.

AdapterUse it for
memoryAdapter()Tests, examples, and process-local development
db(formDb) from @dimah-form/dbSQL through FumaDB
A custom ResponseStoreYour own persistence implementation

Memory

import { dimahForm, memoryAdapter } from "@dimah-form/server";

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

The data disappears when the process restarts.

SQL with FumaDB

npm i @dimah-form/db fumadb
npm i -D tsx

Copy the schema for your ORM, or generate it with the CLI and re-add the indexes. FumaDB's generator does not emit secondary indexes.

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",
      }),
    },
  }),
);

Create a FumaDB client for your ORM, then pass it to db():

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,
});

The package README and example app show Drizzle, Prisma, and Kysely client setup.

Generate the schema

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/db/schema.ts

After generating, retain these indexes:

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

Commit the generated schema and apply it with your ORM's normal migration workflow. When upgrading @dimah-form/db, regenerate into a temporary file, review the diff, restore the secondary indexes above, and create a migration; do not replace a production schema blindly.

Custom stores

Pass a ResponseStore implementation directly as database; do not wrap it in db(). The store supports form CRUD, response CRUD, list/count operations, and draft lookup. When a write receives expectedUpdatedAt, throw StoreConflictError on a mismatch so the HTTP layer returns STALE_UPDATE.

The full method contract is ResponseStore.

Next

On this page