{"id":21515409,"url":"https://github.com/danielfgray/pg-sourcerer","last_synced_at":"2026-01-24T06:51:08.125Z","repository":{"id":212510829,"uuid":"731669197","full_name":"DanielFGray/pg-sourcerer","owner":"DanielFGray","description":"generate code from Postgres introspection data","archived":false,"fork":false,"pushed_at":"2024-12-27T22:57:55.000Z","size":197,"stargazers_count":2,"open_issues_count":12,"forks_count":1,"subscribers_count":2,"default_branch":"develop","last_synced_at":"2025-04-09T20:12:08.509Z","etag":null,"topics":["codegen","postgresql"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DanielFGray.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"danielfgray"}},"created_at":"2023-12-14T15:45:39.000Z","updated_at":"2024-12-27T22:57:59.000Z","dependencies_parsed_at":"2024-04-26T19:40:47.956Z","dependency_job_id":"0d1b83d3-a1d7-477c-9fef-7e6e17dab2aa","html_url":"https://github.com/DanielFGray/pg-sourcerer","commit_stats":null,"previous_names":["danielfgray/pg-query-gen","danielfgray/pg-codeforge","danielfgray/pg-sourcerer"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DanielFGray%2Fpg-sourcerer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DanielFGray%2Fpg-sourcerer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DanielFGray%2Fpg-sourcerer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DanielFGray%2Fpg-sourcerer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DanielFGray","download_url":"https://codeload.github.com/DanielFGray/pg-sourcerer/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248103872,"owners_count":21048245,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["codegen","postgresql"],"created_at":"2024-11-23T23:55:09.122Z","updated_at":"2026-01-24T06:51:08.119Z","avatar_url":"https://github.com/DanielFGray.png","language":"JavaScript","funding_links":["https://github.com/sponsors/danielfgray"],"categories":[],"sub_categories":[],"readme":"# pg-sourcerer\n\nPostgreSQL code generation framework with a plugin ecosystem. Introspects your database schema and generates TypeScript types, Zod schemas, Effect Models, and more.\n\nBuilt with [Effect-ts](https://effect.website/) for robust error handling and composability.\n\n## Installation\n\n```bash\nnpm install @danielfgray/pg-sourcerer\n```\n\n## Quick Start\n\n1. Create a config file `pgsourcerer.config.ts`:\n\n```typescript\nimport { defineConfig, typesPlugin, zod } from \"@danielfgray/pg-sourcerer\";\n\nexport default defineConfig({\n  connectionString: process.env.DATABASE_URL,\n  schemas: [\"public\"],\n  outputDir: \"./src/generated\",\n  plugins: [typesPlugin({ outputDir: \"types\" }), zod({ outputDir: \"schemas\" })],\n});\n```\n\n2. Run the generator:\n\n```bash\npgsourcerer generate\n```\n\n## CLI\n\n```\npgsourcerer generate [options]\n\nOptions:\n  -c, --config \u003cpath\u003e   Path to config file\n  -o, --output \u003cdir\u003e    Override output directory\n  -n, --dry-run         Show what would be generated\n  --log-level \u003clevel\u003e   debug | info | none\n```\n\n## Plugins\n\n| Plugin        | Provides                         | Description                               |\n| ------------- | -------------------------------- | ----------------------------------------- |\n| `typesPlugin` | TypeScript interfaces            | `User`, `UserInsert`, `UserUpdate`        |\n| `zod`         | Zod schemas                      | Runtime validation with inferred types    |\n| `arktype`     | ArkType validators               | String-based type syntax with inference   |\n| `valibot`     | Valibot schemas                  | Modular validation with tree-shaking      |\n| `effect`      | Effect SQL Models + Repositories | Models, repos, and optional HTTP API      |\n| `kysely`      | Kysely types + queries           | DB interface + type-safe CRUD functions   |\n| `sqlQueries`  | Raw SQL functions                | Parameterized query helpers               |\n| `httpElysia`  | Elysia routes                    | REST endpoints with TypeBox validation    |\n| `httpExpress` | Express routes                   | REST endpoints with validation middleware |\n| `httpHono`    | Hono routes                      | REST endpoints with standard-validator    |\n| `httpTrpc`    | tRPC routers                     | Type-safe RPC with Zod validation         |\n| `httpOrpc`    | oRPC handlers                    | Lightweight RPC with TypeScript inference |\n\n## What Gets Generated\n\nGiven a PostgreSQL table like:\n\n```sql\ncreate type app_public.user_role as enum('admin', 'moderator', 'user');\ncreate domain app_public.username as citext check(length(value) \u003e= 2 and length(value) \u003c= 24 and value ~ '^[a-zA-Z][a-zA-Z0-9_-]+$');\ncreate domain app_public.url as text check(value ~ '^https?://\\S+');\n\ncreate table app_public.users (\n  id uuid primary key default gen_random_uuid(),\n  username app_public.username not null unique,\n  name text,\n  avatar_url app_public.url,\n  role app_public.user_role not null default 'user',\n  bio text not null check(length(bio) \u003c= 4000) default '',\n  is_verified boolean not null default false,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now()\n);\n\nalter table app_public.users enable row level security;\ncreate unique index on app_public.users (username);\ncreate index on app_public.users (created_at desc);\n\ngrant\n  select,\n  update(username, name, bio, avatar_url)\n  on app_public.users to :DATABASE_VISITOR;\n```\n\nEach plugin generates different artifacts:\n\n### `typesPlugin` — TypeScript Interfaces\n\n```typescript\nimport type { UserRole } from \"./UserRole.js\";\n\nexport interface User {\n  id: string;\n  username: string;\n  name?: string | null;\n  avatar_url?: string | null;\n  role: UserRole;\n  bio: string;\n  is_verified: boolean;\n  created_at: Date;\n  updated_at: Date;\n}\n\nexport interface UserUpdate {\n  username?: string;\n  name?: string | null;\n  avatar_url?: string | null;\n  bio?: string;\n}\n```\n\n### `zodPlugin` — Zod Schemas\n\n```typescript\n// UserRole.ts\nimport { z } from \"zod\";\n\nexport const UserRole = z.enum([\"admin\", \"moderator\", \"user\"]);\n\nexport type UserRole = z.infer\u003ctypeof UserRole\u003e;\n\n// User.ts\nimport { z } from \"zod\";\nimport { UserRole } from \"./UserRole.js\";\n\nexport const User = z.object({\n  id: z.string().uuid(),\n  username: z.string(),\n  name: z.string().nullable().optional(),\n  avatar_url: z.string().nullable().optional(),\n  role: UserRole,\n  bio: z.string(),\n  is_verified: z.boolean(),\n  created_at: z.coerce.date(),\n  updated_at: z.coerce.date(),\n});\n\nexport type User = z.infer\u003ctypeof User\u003e;\n\nexport const UserUpdate = z.object({\n  id: z.string().uuid(),\n  username: z.string().optional(),\n  name: z.string().nullable().optional(),\n  avatar_url: z.string().nullable().optional(),\n  bio: z.string().optional(),\n});\n\nexport type UserUpdate = z.infer\u003ctypeof UserUpdate\u003e;\n```\n\n### `arktypePlugin` — ArkType Validators\n\n```typescript\n// UserRole.ts\nimport { type } from \"arktype\";\n\nexport const UserRole = type(\"'admin' | 'moderator' | 'user'\");\n\nexport type UserRole = typeof UserRole.infer;\n\n// User.ts\nimport { type } from \"arktype\";\nimport { UserRole } from \"./UserRole.js\";\n\nexport const User = type({\n  id: \"string.uuid\",\n  username: \"string\",\n  \"name?\": \"string | null\",\n  \"avatar_url?\": \"string | null\",\n  role: UserRole,\n  bio: \"string\",\n  is_verified: \"boolean\",\n  created_at: \"Date\",\n  updated_at: \"Date\",\n});\n\nexport type User = typeof User.infer;\n\nexport const UserUpdate = type({\n  \"username?\": \"string\",\n  \"name?\": \"string | null\",\n  \"avatar_url?\": \"string | null\",\n  \"bio?\": \"string\",\n});\n\nexport type UserUpdate = typeof UserUpdate.infer;\n```\n\n### `valibotPlugin` — Valibot Schemas\n\n```typescript\n// UserRole.ts\nimport * as v from \"valibot\";\n\nexport const UserRole = v.picklist([\"admin\", \"moderator\", \"user\"]);\n\nexport type UserRole = v.InferOutput\u003ctypeof UserRole\u003e;\n\n// User.ts\nimport * as v from \"valibot\";\nimport { UserRole } from \"./UserRole.js\";\n\nexport const User = v.object({\n  id: v.pipe(v.string(), v.uuid()),\n  username: v.string(),\n  name: v.optional(v.nullable(v.string())),\n  avatar_url: v.optional(v.nullable(v.string())),\n  role: UserRole,\n  bio: v.string(),\n  is_verified: v.boolean(),\n  created_at: v.date(),\n  updated_at: v.date(),\n});\n\nexport type User = v.InferOutput\u003ctypeof User\u003e;\n\nexport const UserUpdate = v.object({\n  username: v.optional(v.string()),\n  name: v.optional(v.nullable(v.string())),\n  avatar_url: v.optional(v.nullable(v.string())),\n  bio: v.optional(v.string()),\n});\n\nexport type UserUpdate = v.InferOutput\u003ctypeof UserUpdate\u003e;\n```\n\n### `kysely` — Kysely Types + Query Builders\n\nThe unified `kysely` plugin generates both type definitions and query functions:\n\n```typescript\n// DB interface (db.ts)\nimport type { Generated, ColumnType } from \"kysely\";\n\nexport type UserRole = \"admin\" | \"moderator\" | \"user\";\n\nexport interface UsersTable {\n  id: Generated\u003cstring\u003e;\n  username: string;\n  name: string | null;\n  avatar_url: string | null;\n  role: UserRole;\n  bio: string;\n  is_verified: boolean;\n  created_at: Generated\u003cDate\u003e;\n  updated_at: Generated\u003cDate\u003e;\n}\n\nexport interface DB {\n  \"app_public.users\": UsersTable;\n}\n\n//\nimport { db } from \"../../db.js\";\nimport type { UsersTable } from \"./db.js\";\nimport type { Insertable, Updateable } from \"kysely\";\n\nexport const findById = ({ id }: { id: string }) =\u003e\n  db\n    .selectFrom(\"app_public.users\")\n    .select([\n      \"id\",\n      \"username\",\n      \"name\",\n      \"avatar_url\",\n      \"role\",\n      \"bio\",\n      \"is_verified\",\n      \"created_at\",\n      \"updated_at\",\n    ])\n    .where(\"id\", \"=\", id)\n    .executeTakeFirst();\n\nexport const create = ({ data }: { data: Insertable\u003cUsersTable\u003e }) =\u003e\n  db.insertInto(\"app_public.users\").values(data).returningAll().executeTakeFirstOrThrow();\n\nexport const update = ({ id, data }: { id: string; data: Updateable\u003cUsersTable\u003e }) =\u003e\n  db\n    .updateTable(\"app_public.users\")\n    .set(data)\n    .where(\"id\", \"=\", id)\n    .returningAll()\n    .executeTakeFirstOrThrow();\n\nexport const findByUsername = ({ username }: { username: string }) =\u003e\n  db\n    .selectFrom(\"app_public.users\")\n    .select([\n      \"id\",\n      \"username\",\n      \"name\",\n      \"avatar_url\",\n      \"role\",\n      \"bio\",\n      \"is_verified\",\n      \"created_at\",\n      \"updated_at\",\n    ])\n    .where(\"username\", \"=\", username)\n    .executeTakeFirst();\n```\n\n### `sqlQueries` — Raw SQL Query Functions\n\nwith `sqlQueries({ sqlStyle: \"tag\" })`\n\n```typescript\nimport { sql } from \"../../db.js\";\nimport type { User } from \"../types/User.js\";\n\nexport const update = ({\n  id,\n  ...fields\n}: Pick\u003cUser, \"id\"\u003e \u0026 Partial\u003cPick\u003cUser, \"username\" | \"name\" | \"bio\" | \"avatarUrl\"\u003e\u003e) =\u003e\n  sql`update users set ${sql(user, [\"username\" | \"name\" | \"bio\" | \"avatarUrl\"])} where id = ${id}`;\n\nexport const latest = ({ limit = 50, offset = 0 }: { limit?: number; offset?: number }) =\u003e\n  sql\u003cUser[]\u003e`\n    select id, username, name, avatar_url, role, bio, is_verified, created_at, updated_at\n    from app_public.users order by created_at desc limit ${limit} offset ${offset}`;\n\nexport async function findByUsername({ username }: { username: NonNullable\u003cUser[\"username\"]\u003e }) {\n  const [result] = await sql\u003cUser[]\u003e`\n    select id, username, name, avatar_url, role, bio, is_verified, created_at, updated_at\n    from app_public.users where username = ${username}`;\n  return result;\n}\n```\n\nnot using tagged templates? got you covered with `sqlQueries({ sqlStyle: \"string\" })`\n\n### `effect` — Effect SQL Models + Repositories\n\nThe `effect` plugin generates Model classes, optional Repositories, and optional HTTP APIs.\n\n```typescript\n// users/model.ts\nimport { Model } from \"@effect/sql\";\nimport { Schema as S } from \"effect\";\n\nexport class User extends Model.Class\u003cUser\u003e(\"User\")({\n  id: Model.Generated(S.UUID),\n  username: S.String,\n  name: S.NullOr(S.String),\n  role: S.Union(S.Literal(\"admin\"), S.Literal(\"moderator\"), S.Literal(\"user\")),\n  bio: S.String,\n  isVerified: S.Boolean,\n  createdAt: Model.DateTimeInsertFromDate,\n  updatedAt: Model.DateTimeUpdateFromDate,\n}) {}\n\n// users/service.ts\nimport { Model, SqlClient } from \"@effect/sql\";\nimport { User } from \"./User.js\";\n\nexport class UserRepo extends Effect.Service\u003cUserRepo\u003e()(\"UserRepo\", {\n  effect: Effect.gen(function* () {\n    const sql = yield* SqlClient.SqlClient;\n    const queries = {\n      findById: ({ id }: { id: string }) =\u003e db\n        .selectFrom(\"app_public.users\")\n        .select([\"id\", \"username\", \"name\", \"avatar_url\", \"role\", \"bio\", \"is_verified\", \"created_at\", \"updated_at\"])\n        .where(\"id\", \"=\", id),\n\n      create: ({ data }: { data: Insertable\u003cUsersTable\u003e }) =\u003e\n        db.insertInto(\"app_public.users\").values(data).returningAll(),\n\n      update: ({ id, data }: { id: string; data: Updateable\u003cUsersTable\u003e }) =\u003e\n        db.updateTable(\"app_public.users\").set(data).where(\"id\", \"=\", id).returningAll(),,\n\n      findByUsername: ({ username }: { username: string }) =\u003e db\n        .selectFrom(\"app_public.users\")\n        .select([\"id\", \"username\", \"name\", \"avatar_url\", \"role\", \"bio\", \"is_verified\", \"created_at\", \"updated_at\"])\n        .where(\"username\", \"=\", username),\n\n      latest: ({ offset = 0, limit = 50 }: { offset: number; limit: number }) =\u003e db\n        .selectFrom(\"app_public.users\")\n        .select([\"id\", \"username\", \"name\", \"avatar_url\", \"role\", \"bio\", \"is_verified\", \"created_at\", \"updated_at\"])\n        .orderBy(\"created_at\", \"desc\")\n        .limit(limit).offset(offset),\n    }\n    return { ...queries };\n  }),\n}) {}\n```\n\n### `httpElysia` — Elysia REST Routes\n\n```typescript\nimport { Elysia, t } from \"elysia\";\nimport { findUserById, findUserManys, getUserByUsername } from \"../queries/User.js\";\n\nexport const userRoutes = new Elysia({ prefix: \"/api/users\" })\n  .get(\n    \"/:id\",\n    async ({ params, status }) =\u003e {\n      const result = await findUserById({ id: params.id });\n      if (!result) return status(404, \"Not found\");\n      return result;\n    },\n    { params: t.Object({ id: t.String() }) },\n  )\n  .get(\n    \"/\",\n    async ({ query }) =\u003e {\n      return await latest({ limit: query.limit, offset: query.offset });\n    },\n    { query: t.Object({ limit: t.Optional(t.Numeric()), offset: t.Optional(t.Numeric()) }) },\n  )\n  .get(\n    \"/by-username/:username\",\n    async ({ params, status }) =\u003e {\n      const result = await getUserByUsername({ username: params.username });\n      if (!result) return status(404, \"Not found\");\n      return result;\n    },\n    { params: t.Object({ username: t.String() }) },\n  );\n```\n\n### `httpExpress` — Express REST Routes\n\n```typescript\nimport { Router } from \"express\";\nimport { findUserById, listUsers, updateUser } from \"../sql-queries/User.js\";\nimport { User, UserUpdate } from \"../schemas/User.js\";\n\nexport const userRoutes = Router();\n\nuserRoutes.get(\"/:id\", async (req, res) =\u003e {\n  const { params } = z.object({\n    params: z.object({ id: User.shape.id })\n  }).parse({ params: req.params }})\n  const result = await findUserById({ id: params.id });\n  if (!result) return res.status(404).json({ error: \"Not found\" });\n  return res.json(result);\n});\n\nuserRoutes.get(\"/\", async (req, res) =\u003e {\n  const { query } = z.object({\n    query: z.object({\n      limit: z.coerce.number().optional(),\n      offset: z.coerce.number().optional(),\n    })\n  }).parse({ query: req.query }})\n  return res.json(await latest(query);\n});\n\nuserRoutes.put(\"/:id\", async (req, res) =\u003e {\n  const { success,  } = z.object({\n    params: z.object({ id: User.shape.id })\n    body: UserUpdate,\n  }).safeParse({ params: req.params, body: req.body }})\n  if (!success) return res.status(400);\n  const result = await updateUser({ id, ...data });\n  return res.json(result);\n});\n```\n\n### `httpHono` — Hono REST Routes\n\n```typescript\nimport { Hono } from \"hono\";\nimport { sValidator } from \"@hono/standard-validator\";\nimport { z } from \"zod\";\nimport { findUserById, listUsers, updateUser } from \"../sql-queries/User.js\";\nimport { UserUpdate } from \"../schemas/User.js\";\n\nexport const userRoutes = new Hono()\n  .get(\"/:id\", async c =\u003e {\n    const id = c.req.param(\"id\");\n    const result = await findUserById({ id });\n    if (!result) return c.json({ error: \"Not found\" }, 404);\n    return c.json(result);\n  })\n  .get(\n    \"/\",\n    sValidator(\n      \"query\",\n      z.object({\n        limit: z.coerce.number().optional(),\n        offset: z.coerce.number().optional(),\n      }),\n    ),\n    async c =\u003e {\n      const { limit, offset } = c.req.valid(\"query\");\n      return c.json(await latest({ limit, offset }));\n    },\n  )\n  .put(\"/:id\", sValidator(\"json\", UserUpdate), async c =\u003e {\n    const id = c.req.param(\"id\");\n    const data = c.req.valid(\"json\");\n    const result = await updateUser({ id, ...data });\n    return c.json(result);\n  });\n```\n\n### `httpTrpc` — tRPC Routers\n\n```typescript\nimport { z } from \"zod\";\nimport { router, publicProcedure } from \"../trpc.js\";\nimport { findUserById, listUsers, getUserByUsername } from \"../sql-queries/User.js\";\n\nexport const userRouter = router({\n  findUserById: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input }) =\u003e {\n    return await findUserById({ id: input.id });\n  }),\n\n  listUsers: publicProcedure\n    .input(z.object({ limit: z.coerce.number().optional(), offset: z.coerce.number().optional() }))\n    .query(async ({ input }) =\u003e {\n      return await listUsers({ limit: input.limit, offset: input.offset });\n    }),\n\n  getUserByUsername: publicProcedure\n    .input(z.object({ username: z.string() }))\n    .query(async ({ input }) =\u003e {\n      return await getUserByUsername({ username: input.username });\n    }),\n});\n```\n\n### `httpOrpc` — oRPC Handlers\n\n```typescript\nimport { findUserById, listUsers, getUserByUsername } from \"../sql-queries/User.js\";\nimport { os, type } from \"@orpc/server\";\n\nexport const findById = os\n  .input(type\u003c{ id: string }\u003e())\n  .handler(async ({ input }) =\u003e await findUserById(input));\n\nexport const list = os\n  .input(type\u003c{ limit?: number; offset?: number }\u003e())\n  .handler(async ({ input }) =\u003e await listUsers(input));\n\nexport const findByUsername = os\n  .input(type\u003c{ username: string }\u003e())\n  .handler(async ({ input }) =\u003e await getUserByUsername(input));\n\nexport const userRouter = { findById, list, findByUsername };\n```\n\n## Smart Tags\n\nConfigure generation via PostgreSQL comments:\n\n```sql\n-- Rename entity\nCOMMENT ON TABLE users IS '{\"sourcerer\": {\"name\": \"Account\"}}';\n\n-- Omit from generation\nCOMMENT ON COLUMN users.password_hash IS '{\"sourcerer\": {\"omit\": true}}';\n\n-- Omit from specific shapes\nCOMMENT ON COLUMN users.created_at IS '{\"sourcerer\": {\"omit\": [\"insert\", \"update\"]}}';\n\n-- Custom relation names\nCOMMENT ON CONSTRAINT posts_author_fkey ON posts IS\n  '{\"sourcerer\": {\"fieldName\": \"author\", \"foreignFieldName\": \"posts\"}}';\n```\n\n## Type Hints\n\nOverride type mappings in your config:\n\n```typescript\ndefineConfig({\n  // ...\n  typeHints: [\n    {\n      match: { pgType: \"uuid\" },\n      hints: { ts: \"string\", zod: \"z.string().uuid()\" },\n    },\n    {\n      match: { table: \"users\", column: \"email\" },\n      hints: { ts: \"Email\", zod: \"emailSchema\", import: { Email: \"./branded.js\" } },\n    },\n  ],\n});\n```\n\n## Writing Plugins\n\nPlugins generate code from the introspected database schema. Use `definePlugin` for a simple, synchronous API.\n\n### Minimal Example\n\n```typescript\nimport { definePlugin, conjure, Schema as S } from \"@danielfgray/pg-sourcerer\";\n\nconst { ts, exp } = conjure;\n\nexport const myPlugin = definePlugin({\n  name: \"my-plugin\",\n  provides: [\"my-types\"],\n  configSchema: S.Struct({\n    outputDir: S.String,\n  }),\n  inflection: {\n    outputFile: ctx =\u003e `${ctx.entityName}.ts`,\n    symbolName: (entity, kind) =\u003e `${entity}${kind}`,\n  },\n\n  run: (ctx, config) =\u003e {\n    ctx.ir.entities.forEach((entity, name) =\u003e {\n      // Build interface properties from row shape\n      const props = entity.shapes.row.fields.map(field =\u003e ({\n        name: field.name,\n        type: field.nullable ? ts.union(ts.string(), ts.null()) : ts.string(),\n        optional: field.optional,\n      }));\n\n      // Create exported interface with symbol tracking\n      const statement = exp.interface(\n        `${name}Row`,\n        { capability: \"my-types\", entity: name, shape: \"row\" },\n        props,\n      );\n\n      // Emit file\n      ctx\n        .file(`${config.outputDir}/${name}.ts`)\n        .header(\"// Auto-generated\\n\")\n        .ast(conjure.symbolProgram(statement))\n        .emit();\n    });\n  },\n});\n```\n\n### Plugin Context\n\nThe `ctx` object provides:\n\n| Property                     | Description                                         |\n| ---------------------------- | --------------------------------------------------- |\n| `ctx.ir`                     | Semantic IR with `entities`, `enums`, `extensions`  |\n| `ctx.inflection`             | Naming utilities (`camelCase`, `singularize`, etc.) |\n| `ctx.typeHints`              | User-configured type overrides                      |\n| `ctx.file(path)`             | Create a `FileBuilder` for structured emission      |\n| `ctx.emit(path, content)`    | Emit raw string content                             |\n| `ctx.getArtifact(cap)`       | Read data from upstream plugins                     |\n| `ctx.setArtifact(cap, data)` | Share data with downstream plugins                  |\n\n### Conjure API\n\nConjure builds AST nodes for code generation:\n\n```typescript\n// Method chains: z.string().uuid()\nconjure.id(\"z\").method(\"string\").method(\"uuid\").build();\n\n// Object literals: { path: \"/users\", method: \"GET\" }\nconjure.obj().prop(\"path\", conjure.str(\"/users\")).prop(\"method\", conjure.str(\"GET\")).build();\n\n// TypeScript types\nconjure.ts.string(); // string\nconjure.ts.ref(\"User\"); // User\nconjure.ts.array(conjure.ts.string()); // string[]\nconjure.ts.union(conjure.ts.string(), ts.null()); // string | null\n\n// Statements\nconjure.stmt.const(\"x\", conjure.num(42)); // const x = 42\nconjure.stmt.return(conjure.id(\"result\")); // return result\n\n// Exports with symbol tracking (for import resolution)\nexp.interface(\"UserRow\", symbolCtx, properties);\nexp.const(\"UserSchema\", symbolCtx, schemaExpr);\nexp.typeAlias(\"UserId\", symbolCtx, ts.string());\n\n// Print to code string\nconjure.print(node);\n```\n\n### Depending on Other Plugins\n\nUse `requires` to depend on capabilities from other plugins:\n\n```typescript\ndefinePlugin({\n  name: \"zod-schemas\",\n  requires: [\"types\"], // Must run after types plugin\n  provides: [\"schemas:zod\"],\n  // ...\n});\n```\n\nAccess upstream artifacts:\n\n```typescript\nrun: ctx =\u003e {\n  const typesArtifact = ctx.getArtifact(\"types\");\n  // Use data from types plugin\n};\n```\n\n## Development\n\n```bash\n# Clone and install\ngit clone https://github.com/danielfgray/pg-sourcerer\ncd pg-sourcerer\nnpm install\n\n# Run tests\ncd packages/pg-sourcerer\nnpm test\n\n# Try the example\ncd packages/example\nnpm run init      # Start Postgres, run migrations\nnpm run generate  # Generate code\n```\n\n## License\n\nMIT — see [LICENSE](./LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanielfgray%2Fpg-sourcerer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanielfgray%2Fpg-sourcerer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanielfgray%2Fpg-sourcerer/lists"}