@rstore/nuxt-drizzle
Version:
Rstore + Nuxt + Drizzle
355 lines (350 loc) • 13.8 kB
JavaScript
import fs from 'node:fs';
import { defineNuxtModule, useLogger, createResolver, hasNuxtModule, installModule, addServerHandler, addImportsDir, addServerTemplate, addTemplate, addTypeTemplate, updateTemplates } from '@nuxt/kit';
import { isTable, getTableName, is, Relations, createTableRelationsHelpers, One, Many } from 'drizzle-orm';
import { createJiti } from 'jiti';
import path from 'pathe';
const module = defineNuxtModule({
meta: {
name: "rstore-nuxt-drizzle",
configKey: "rstoreDrizzle"
},
defaults: {},
async setup(options, nuxt) {
const log = useLogger("rstore-drizzle");
const { resolve } = createResolver(import.meta.url);
if (!hasNuxtModule("@rstore/nuxt")) {
await installModule("@rstore/nuxt");
}
addServerHandler({
handler: resolve("./runtime/server/api/index.get"),
route: "/api/rstore/:model",
method: "get"
});
addServerHandler({
handler: resolve("./runtime/server/api/index.post"),
route: "/api/rstore/:model",
method: "post"
});
addServerHandler({
handler: resolve("./runtime/server/api/[key]/index.get"),
route: "/api/rstore/:model/:key",
method: "get"
});
addServerHandler({
handler: resolve("./runtime/server/api/[key]/index.patch"),
route: "/api/rstore/:model/:key",
method: "patch"
});
addServerHandler({
handler: resolve("./runtime/server/api/[key]/index.delete"),
route: "/api/rstore/:model/:key",
method: "delete"
});
addImportsDir(resolve("./runtime/utils"));
const jiti = createJiti(import.meta.url, {
moduleCache: false
});
const drizzleConfigPath = options.drizzleConfigPath ?? "drizzle.config.ts";
const drizzleConfigFullPath = path.resolve(nuxt.options.rootDir, drizzleConfigPath);
if (!fs.existsSync(drizzleConfigFullPath)) {
log.warn(`Drizzle config not found, skipping drizzle module (looking for ${drizzleConfigPath})`);
return;
}
const drizzleConfig = (await jiti.import(drizzleConfigFullPath)).default;
if (!drizzleConfig) {
throw new Error("No drizzle config found");
}
if (typeof drizzleConfig.schema !== "string") {
throw new TypeError("Drizzle schema must be a single file path");
}
const drizzleSchemaPath = path.resolve(nuxt.options.rootDir, drizzleConfig.schema);
function useDedupePromise(fn) {
let promise = null;
return () => {
if (!promise) {
promise = fn();
promise.finally(() => {
promise = null;
});
}
return promise;
};
}
const getModelsFormDrizzleSchema = useDedupePromise(async () => {
const schema = { ...await jiti.import(drizzleSchemaPath) };
const models = [];
const modelsByTable = /* @__PURE__ */ new WeakMap();
const tables = [];
const relationsList = [];
for (const key in schema) {
const schemaItem = schema[key];
if (isTable(schemaItem)) {
let config;
switch (drizzleConfig.dialect) {
case "postgresql": {
const { getTableConfig } = await import('drizzle-orm/pg-core');
config = getTableConfig(schemaItem);
break;
}
case "mysql": {
const { getTableConfig: getMySqlTableConfig } = await import('drizzle-orm/mysql-core');
config = getMySqlTableConfig(schemaItem);
break;
}
case "sqlite": {
const { getTableConfig: getSqliteTableConfig } = await import('drizzle-orm/sqlite-core');
config = getSqliteTableConfig(schemaItem);
break;
}
case "singlestore": {
const { getTableConfig: getSingleStoreTableConfig } = await import('drizzle-orm/singlestore-core');
config = getSingleStoreTableConfig(schemaItem);
break;
}
}
const table = {
key,
table: schemaItem,
tableName: getTableName(schemaItem),
config
};
tables.push(table);
} else if (is(schemaItem, Relations)) {
relationsList.push(schemaItem);
}
}
for (const { key, table, tableName, config } of tables) {
const model = {
name: key,
meta: {
scopeId: "rstore-drizzle",
table: tableName,
primaryKeys: config?.primaryKeys?.length ? config.primaryKeys : config?.columns?.filter((col) => col.primary).map((col) => col.keyAsName ? col.name : col.key)
}
};
models.push(model);
modelsByTable.set(table, model);
}
for (const relations of relationsList) {
const model = modelsByTable.get(relations.table);
if (!model) {
throw new Error(`Model not found for table ${relations.table}`);
}
model.relations ??= {};
const config = relations.config(createTableRelationsHelpers(relations.table));
const explicitOneRelations = [];
const implicitOneRelations = [];
const implicitManyRelations = [];
for (const key in config) {
const relation = config[key];
if (is(relation, One)) {
if (relation.config) {
const fields = relation.config.fields;
if (!fields[0]) {
implicitOneRelations.push({ key, relation });
continue;
}
if (fields.length > 1) {
throw new Error(`Relations with multiple fields are not supported yet, see https://github.com/Akryum/rstore/issues/7`);
}
const references = relation.config.references;
if (!references[0]) {
implicitOneRelations.push({ key, relation });
continue;
}
if (references.length > 1) {
throw new Error(`Relations with multiple references are not supported yet, see https://github.com/Akryum/rstore/issues/7`);
}
const targetModel = modelsByTable.get(relation.referencedTable);
if (!targetModel) {
throw new Error(`Target model not found for table ${relation.referencedTableName}`);
}
model.relations[key] = {
to: {
[targetModel.name]: {
on: references[0].name,
eq: fields[0].name
}
}
};
explicitOneRelations.push(relation);
} else {
implicitOneRelations.push({ key, relation });
}
} else if (is(relation, Many)) {
implicitManyRelations.push({ key, relation });
}
}
for (const { key, relation } of implicitOneRelations) {
if (relation.relationName) {
const targetRelation = explicitOneRelations.find((r) => r.relationName === relation.relationName);
if (!targetRelation) {
throw new Error(`Explicit relation not found for ${relation.relationName}`);
}
const targetModel = modelsByTable.get(targetRelation.referencedTable);
if (!targetModel) {
throw new Error(`Target model not found for table ${targetRelation.referencedTableName}`);
}
model.relations[key] = {
to: {
[targetModel.name]: {
on: targetRelation.config.fields[0].name,
eq: targetRelation.config.references[0].name
}
}
};
} else {
const targetModel = modelsByTable.get(relation.referencedTable);
if (!targetModel) {
throw new Error(`Target model not found for table ${relation.referencedTableName}`);
}
if (!targetModel.relations) {
throw new Error(`Target model ${targetModel.name} has no relations`);
}
let newRelation;
for (const relationKey in targetModel.relations) {
for (const modelName in targetModel.relations[relationKey].to) {
if (modelName === model.name) {
newRelation = {
to: {
[targetModel.name]: {
on: targetModel.relations[relationKey].to[modelName].eq,
eq: targetModel.relations[relationKey].to[modelName].on
}
}
};
break;
}
}
}
if (!newRelation) {
throw new Error(`Reference relation not found for ${model.name}.${key}`);
}
model.relations[key] = newRelation;
}
}
for (const { key, relation } of implicitManyRelations) {
if (relation.relationName) {
const targetRelation = explicitOneRelations.find((r) => r.relationName === relation.relationName);
if (!targetRelation) {
throw new Error(`Explicit relation not found for ${relation.relationName}`);
}
const targetModel = modelsByTable.get(targetRelation.referencedTable);
if (!targetModel) {
throw new Error(`Target model not found for table ${targetRelation.referencedTableName}`);
}
model.relations[key] = {
to: {
[targetModel.name]: {
on: targetRelation.config.fields[0].name,
eq: targetRelation.config.references[0].name
}
},
many: true
};
} else {
const targetModel = modelsByTable.get(relation.referencedTable);
if (!targetModel) {
throw new Error(`Target model not found for table ${relation.referencedTableName}`);
}
if (!targetModel.relations) {
throw new Error(`Target model ${targetModel.name} has no relations`);
}
let newRelation;
for (const relationKey in targetModel.relations) {
for (const modelName in targetModel.relations[relationKey].to) {
if (modelName === model.name) {
newRelation = {
to: {
[targetModel.name]: {
on: targetModel.relations[relationKey].to[modelName].eq,
eq: targetModel.relations[relationKey].to[modelName].on
}
},
many: true
};
break;
}
}
}
if (!newRelation) {
throw new Error(`Reference relation not found for ${model.name}.${key}`);
}
model.relations[key] = newRelation;
}
}
}
return models;
});
addServerTemplate({
filename: "$rstore-drizzle-server-utils.js",
getContents: async () => {
jiti.cache = {};
const models = await getModelsFormDrizzleSchema();
const modelMetas = {};
for (const model of models) {
modelMetas[model.name] = model.meta;
}
return `import * as schema from '${drizzleSchemaPath}'
import { ${options.drizzleImport?.default?.name ?? "useDrizzle"} as _drizzleDefault } from '${options.drizzleImport?.default?.from ?? "~~/server/utils/drizzle"}'
export const tables = schema
export const modelMetas = ${JSON.stringify(modelMetas, null, 2)}
export const dialect = '${drizzleConfig.dialect}'
export const useDrizzles = {
default: _drizzleDefault,
}`;
}
});
addTemplate({
filename: "$rstore-drizzle-models.js",
getContents: async () => {
const models = await getModelsFormDrizzleSchema();
return `export default [${models.map((model) => {
let code = `{`;
code += `name: '${model.name}',`;
code += `meta: ${JSON.stringify(model.meta)},`;
if (model.relations) {
code += `relations: ${JSON.stringify(model.relations)},`;
}
code += `getKey: (item) => ${model.meta?.primaryKeys?.length ? `(${model.meta.primaryKeys.map((key) => `item.${key}`).join(" + ")})` : "item.id"},`;
code += `}`;
return code;
}).join(",\n")}]`;
}
});
addTypeTemplate({
filename: "$rstore-drizzle-models.d.ts",
getContents: async () => {
const models = await getModelsFormDrizzleSchema();
return `import { defineItemType } from '@rstore/vue'
import * as schema from '${drizzleSchemaPath}'
export default [
${models.map((model) => {
let code = `defineItemType<typeof schema.${model.name}.$inferSelect>().model({`;
code += `name: '${model.name}',`;
code += `meta: ${JSON.stringify(model.meta)},`;
if (model.relations) {
code += `relations: ${JSON.stringify(model.relations)},`;
}
code += `} as const),`;
return code;
}).join("\n")}
]
`;
}
});
if (nuxt.options.dev) {
nuxt.hook("nitro:init", (nitro) => {
nitro.hooks.hook("dev:reload", () => {
updateTemplates({
filter: (template) => template.filename === "$rstore-drizzle-models.js" || template.filename === "$rstore-drizzle-models.d.ts"
});
});
});
}
const { addModelImport, addPluginImport } = await import('@rstore/nuxt/api');
addModelImport(nuxt, "#build/$rstore-drizzle-models.js");
addPluginImport(nuxt, resolve("./runtime/plugin"));
}
});
export { module as default };