Getting started

Installation

npm install @rjdellecese/confect
yarn add @rjdellecese/confect
pnpm add @rjdellecese/confect

exactOptionalPropertyTypes

Normally when using the Effect schema library, it's recommended to set exactOptionalPropertyTypes in your tsconfig.json to true. However, this configuration is not supported by convex-js at the moment, so to use Confect, you must set it to false instead.

To understand the implications of this, see this explanation in the Effect docs.

Usage

1. Define your database schema

Not every Effect Schema is valid for use in Confect. See Schema restrictions for more information about what's permitted and what's not.

https://github.com/rjdellecese/confect/blob/main/example/convex/schema.ts
import { Id, defineSchema, defineTable } from "@rjdellecese/confect/server";
import { Schema } from "effect";

type Tag = {
  readonly name: string;
  readonly tags: readonly Tag[];
};

const Tag = Schema.Struct({
  name: Schema.String,
  tags: Schema.Array(Schema.suspend((): Schema.Schema<Tag> => Tag)),
});

export const confectSchema = defineSchema({
  notes: defineTable(
    Schema.Struct({
      userId: Schema.optional(Id.Id("users")),
      text: Schema.String.pipe(Schema.maxLength(100)),
      tag: Schema.optional(Schema.String),
      author: Schema.optional(
        Schema.Struct({
          role: Schema.Literal("admin", "user"),
          name: Schema.String,
        }),
      ),
      embedding: Schema.optional(Schema.Array(Schema.Number)),
    }),
  )
    .index("by_text", ["text"])
    .index("by_role", ["author.role"])
    .searchIndex("text", {
      searchField: "text",
      filterFields: ["tag"],
    })
    .vectorIndex("embedding", {
      vectorField: "embedding",
      filterFields: ["author.name", "tag"],
      dimensions: 1536,
    }),
  users: defineTable(
    Schema.Struct({
      username: Schema.String,
    }),
  ),
  tags: defineTable(Tag),
});

export default confectSchema.convexSchemaDefinition;

2. Generate your Convex function constructors and types.

https://github.com/rjdellecese/confect/blob/main/example/convex/confect.ts
import {
  ConfectActionCtx as ConfectActionCtxService,
  type ConfectActionCtx as ConfectActionCtxType,
  type ConfectDataModelFromConfectSchemaDefinition,
  type ConfectDoc as ConfectDocType,
  ConfectMutationCtx as ConfectMutationCtxService,
  type ConfectMutationCtx as ConfectMutationCtxType,
  ConfectQueryCtx as ConfectQueryCtxService,
  type ConfectQueryCtx as ConfectQueryCtxType,
  type TableNamesInConfectDataModel,
  makeFunctions,
} from "@rjdellecese/confect/server";

import { confectSchema } from "./schema";

export const {
  action,
  internalAction,
  internalMutation,
  internalQuery,
  mutation,
  query,
} = makeFunctions(confectSchema);

type ConfectSchema = typeof confectSchema;

type ConfectDataModel =
  ConfectDataModelFromConfectSchemaDefinition<ConfectSchema>;

export type ConfectDoc<
  TableName extends TableNamesInConfectDataModel<ConfectDataModel>,
> = ConfectDocType<ConfectDataModel, TableName>;

export const ConfectQueryCtx = ConfectQueryCtxService<ConfectDataModel>();
export type ConfectQueryCtx = ConfectQueryCtxType<ConfectDataModel>;

export const ConfectMutationCtx = ConfectMutationCtxService<ConfectDataModel>();
export type ConfectMutationCtx = ConfectMutationCtxType<ConfectDataModel>;

export const ConfectActionCtx = ConfectActionCtxService<ConfectDataModel>();
export type ConfectActionCtx = ConfectActionCtxType<ConfectDataModel>;

3. Write some Convex functions!

https://github.com/rjdellecese/confect/blob/main/example/convex/functions.ts
import { Effect } from "effect";
import {
  ConfectMutationCtx,
  ConfectQueryCtx,
  action,
  mutation,
  query,
} from "./confect";
import {
  DeleteNoteArgs,
  DeleteNoteResult,
  GetFirstArgs,
  GetFirstResult,
  GetRandomArgs,
  GetRandomResult,
  InsertNoteArgs,
  InsertNoteResult,
  ListNotesArgs,
  ListNotesResult,
} from "./functions.schemas";

export const insertNote = mutation({
  args: InsertNoteArgs,
  returns: InsertNoteResult,
  handler: ({ text }) =>
    Effect.gen(function* () {
      const { db } = yield* ConfectMutationCtx;

      return yield* db.insert("notes", { text });
    }),
});

export const listNotes = query({
  args: ListNotesArgs,
  returns: ListNotesResult,
  handler: () =>
    Effect.gen(function* () {
      const { db } = yield* ConfectQueryCtx;

      return yield* db.query("notes").order("desc").collect();
    }),
});

export const deleteNote = mutation({
  args: DeleteNoteArgs,
  returns: DeleteNoteResult,
  handler: ({ noteId }) =>
    Effect.gen(function* () {
      const { db } = yield* ConfectMutationCtx;

      return yield* db.delete(noteId).pipe(Effect.as(null));
    }),
});

export const getRandom = action({
  args: GetRandomArgs,
  returns: GetRandomResult,
  handler: () => Effect.succeed(Math.random()),
});

export const getFirst = query({
  args: GetFirstArgs,
  returns: GetFirstResult,
  handler: () =>
    Effect.gen(function* () {
      const { db } = yield* ConfectQueryCtx;

      return yield* db.query("notes").first();
    }),
});

Example project

The above files are pulled from a real example project. Check it out here.

Last updated