kitcn
Version:
kitcn - React Query integration and CLI tools for Convex
156 lines (151 loc) • 5.5 kB
JavaScript
//#region src/auth/create-schema.ts
const indexFields = {
account: [
"accountId",
["accountId", "providerId"],
["providerId", "userId"]
],
oauthConsent: [["clientId", "userId"]],
passkey: ["credentialID"],
ratelimit: ["key"],
rateLimit: ["key"],
session: ["expiresAt", ["expiresAt", "userId"]],
user: [["email", "name"], "name"],
verification: ["expiresAt", "identifier"]
};
const cloneTables = (tables) => Object.fromEntries(Object.entries(tables).map(([key, table]) => [key, {
...table,
fields: { ...table.fields }
}]));
const ensureField = (tables, tableKey, fieldKey, fieldPatch) => {
const table = tables[tableKey];
if (!table) return;
const existingField = table.fields[fieldKey];
if (!existingField) {
table.fields[fieldKey] = fieldPatch;
return;
}
table.fields[fieldKey] = {
...fieldPatch,
...existingField,
references: existingField.references ?? fieldPatch.references
};
};
const augmentBetterAuthTables = (sourceTables) => {
const tables = cloneTables(sourceTables);
if (tables.organization && tables.user) {
const organizationReference = {
field: "id",
model: "organization"
};
ensureField(tables, "user", "lastActiveOrganizationId", {
references: organizationReference,
required: false,
type: "string"
});
ensureField(tables, "user", "personalOrganizationId", {
references: organizationReference,
required: false,
type: "string"
});
ensureField(tables, "session", "activeOrganizationId", {
references: organizationReference,
required: false,
type: "string"
});
}
if (tables.team) {
const teamReference = {
field: "id",
model: "team"
};
ensureField(tables, "session", "activeTeamId", {
references: teamReference,
required: false,
type: "string"
});
ensureField(tables, "invitation", "teamId", {
references: teamReference,
required: false,
type: "string"
});
}
return tables;
};
const specialFields = (tables) => Object.fromEntries(Object.entries(tables).map(([key, table]) => {
return [key, Object.fromEntries(Object.entries(table.fields).map(([fieldKey, field]) => [field.fieldName ?? fieldKey, {
...field.sortable ? { sortable: true } : {},
...field.unique ? { unique: true } : {},
...field.references ? { references: field.references } : {}
}]).filter(([_key, value]) => typeof value === "object" ? Object.keys(value).length > 0 : true))];
}).filter(([_key, value]) => typeof value === "object" ? Object.keys(value).length > 0 : true));
const mergedIndexFields = (tables) => Object.fromEntries(Object.entries(tables).map(([key, table]) => {
const resolveIndexField = (fieldKey) => {
const field = table.fields[fieldKey];
return field ? field.fieldName ?? fieldKey : null;
};
const manualIndexes = indexFields[key]?.reduce((indexes, index) => {
if (typeof index === "string") {
const resolved = resolveIndexField(index);
if (resolved) indexes.push(resolved);
return indexes;
}
const resolved = index.map((fieldKey) => resolveIndexField(fieldKey)).filter((fieldName) => fieldName !== null);
if (resolved.length === index.length) indexes.push(resolved);
return indexes;
}, []) || [];
const specialFieldIndexes = Object.keys(specialFields(tables)[key] || {}).filter((index) => !manualIndexes.some((m) => Array.isArray(m) ? m[0] === index : m === index));
return [key, manualIndexes.concat(specialFieldIndexes)];
}));
const createSchema = async ({ exportName = "tables", file, regenerateCommand, tables }) => {
const path = await import(Buffer.from("cGF0aA==", "base64").toString());
if (path.basename(path.resolve(process.cwd(), file ?? "")) === "convex") throw new Error("Better Auth schema must be generated in the Better Auth component directory.");
tables = augmentBetterAuthTables(tables);
let code = `// This file is auto-generated. Do not edit this file manually.
// To regenerate the schema, run:
// \`${regenerateCommand ?? `npx @better-auth/cli generate --output ${file} -y`}\`
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export const ${exportName} = {
`;
for (const [tableKey, table] of Object.entries(tables)) {
const modelName = table.modelName;
const fields = Object.fromEntries(Object.entries(table.fields).filter(([key]) => key !== "id"));
function getType(_name, field) {
const type = field.type;
return {
boolean: "v.boolean()",
date: "v.number()",
json: "v.string()",
number: "v.number()",
"number[]": "v.array(v.number())",
string: "v.string()",
"string[]": "v.array(v.string())"
}[type];
}
const indexes = mergedIndexFields(tables)[tableKey]?.map((index) => {
const indexArray = Array.isArray(index) ? index.sort() : [index];
return `.index("${indexArray.join("_")}", ${JSON.stringify(indexArray)})`;
}) || [];
const schema = `${modelName}: defineTable({
${Object.keys(fields).map((field) => {
const attr = fields[field];
const type = getType(field, attr);
const optional = (fieldSchema) => attr.required ? fieldSchema : `v.optional(v.union(v.null(), ${fieldSchema}))`;
return ` ${attr.fieldName ?? field}: ${optional(type)},`;
}).join("\n")}
})${indexes.length > 0 ? `\n ${indexes.join("\n ")}` : ""},\n`;
code += ` ${schema}`;
}
code += `};
const schema = defineSchema(${exportName});
export default schema;
`;
return {
code,
overwrite: true,
path: file ?? "./schema.ts"
};
};
//#endregion
export { augmentBetterAuthTables, createSchema, indexFields };