From 38febff1779c48f8817f74def8baae4453633034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Moreau?= Date: Mon, 25 Nov 2024 20:57:15 +0100 Subject: [PATCH] Support for mysql in the visualizer (#36) * add mysql support in visualizer --- apps/cli/example/mysql/drizzle.config.ts | 22 +++ apps/cli/example/mysql/schema.ts | 105 ++++++++++++ apps/cli/package.json | 2 +- package-lock.json | 6 +- packages/api/src/mysql/loader/database.ts | 4 +- packages/api/src/mysql/schema.ts | 4 +- packages/api/src/mysql/serializer/snapshot.ts | 2 +- .../api/src/mysql/serializer/typescript.ts | 8 +- packages/visualizer/src/compute.ts | 161 +++++++++++++++++- vscode-extension/CHANGELOG.md | 5 + vscode-extension/package-lock.json | 4 +- vscode-extension/package.json | 2 +- vscode-extension/src/utils.ts | 4 +- 13 files changed, 306 insertions(+), 23 deletions(-) create mode 100644 apps/cli/example/mysql/drizzle.config.ts create mode 100644 apps/cli/example/mysql/schema.ts diff --git a/apps/cli/example/mysql/drizzle.config.ts b/apps/cli/example/mysql/drizzle.config.ts new file mode 100644 index 0000000..19a1246 --- /dev/null +++ b/apps/cli/example/mysql/drizzle.config.ts @@ -0,0 +1,22 @@ +/** + * This is the configuration for the server-side database. + */ + +import { defineConfig } from "drizzle-kit"; + +const base = "./example/mysql"; + +export default defineConfig({ + dialect: "mysql", + dbCredentials: { + url: process.env.ADMIN_DATABASE_URL!, + }, + schema: `${base}/schema.ts`, + out: `${base}/migrations`, + verbose: false, + schemaFilter: ["public"], + casing: "snake_case", + migrations: { + prefix: "timestamp", + }, +}); diff --git a/apps/cli/example/mysql/schema.ts b/apps/cli/example/mysql/schema.ts new file mode 100644 index 0000000..f320322 --- /dev/null +++ b/apps/cli/example/mysql/schema.ts @@ -0,0 +1,105 @@ +import { randomUUID } from "crypto"; + +import { relations, sql, getTableColumns } from "drizzle-orm"; +import { + mysqlTable, + serial, + text, + int, + timestamp, + json, + foreignKey, + primaryKey, + check, + mysqlView, +} from "drizzle-orm/mysql-core"; + +export const users = mysqlTable("users", { + id: serial("id").primaryKey(), + name: text("name"), +}); + +export const usersRelations = relations(users, ({ many }) => ({ + author: many(posts, { relationName: "author" }), + reviewer: many(posts, { relationName: "reviewer" }), +})); + +function generateSlug() { + return randomUUID(); +} + +type PostMetadata = { + source: "mobile_app" | "web_app"; + value: { + id: string; + tags: string[]; + }; +}; + +export const posts = mysqlTable( + "posts", + { + id: serial("id"), + slug: text("slug") + .notNull() + .$default(() => generateSlug()), + status: text("status", { enum: ["draft", "published"] }) + .default("draft") + .notNull(), + content: text("content"), + authorId: int("author_id") + .references(() => users.id) + .notNull(), + reviewerId: int("reviewer_id"), + createdAt: timestamp("created_at", { + mode: "string", + }).defaultNow(), + metadata: json("metadata").$type(), + metadata2: json("metadata2") + .$type() + .default({ + source: "mobile_app", + value: { + id: "123", + tags: ["tag1", "tag2"], + }, + }), + }, + (t) => ({ + p: primaryKey({ name: "my pk", columns: [t.id, t.slug] }), + f: foreignKey({ + name: "my fk", + columns: [t.authorId], + foreignColumns: [users.id], + }), + c: check("not draft", sql`status <> 'draft'`), + }) +); +export const postsView = mysqlView("posts_view").as((qb) => + qb + .select({ + ...getTableColumns(posts), + }) + .from(posts) +); + +export const postsViewUnsecured = mysqlView("posts_view_unsecured").as((qb) => + qb + .select({ + ...getTableColumns(posts), + }) + .from(posts) +); + +export const postsRelations = relations(posts, ({ one }) => ({ + author: one(users, { + fields: [posts.authorId], + references: [users.id], + relationName: "author", + }), + reviewer: one(users, { + fields: [posts.reviewerId], + references: [users.id], + relationName: "reviewer", + }), +})); diff --git a/apps/cli/package.json b/apps/cli/package.json index 5583173..5b6c8f3 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-lab", - "version": "0.7.0", + "version": "0.8.0", "description": "Drizzle Lab CLI", "sideEffects": false, "type": "module", diff --git a/package-lock.json b/package-lock.json index 117b4ab..0e82649 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ }, "apps/cli": { "name": "drizzle-lab", - "version": "0.7.0", + "version": "0.8.0", "license": "MIT", "dependencies": { "@drizzle-lab/api": "*", @@ -102,7 +102,7 @@ } }, "apps/drizzle-run": { - "version": "0.14.0", + "version": "0.16.0", "license": "AGPL-3.0-only", "dependencies": { "@conform-to/react": "^1.1.3", @@ -1619,7 +1619,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", - "dev": true, + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { diff --git a/packages/api/src/mysql/loader/database.ts b/packages/api/src/mysql/loader/database.ts index bd3addd..98bf02d 100644 --- a/packages/api/src/mysql/loader/database.ts +++ b/packages/api/src/mysql/loader/database.ts @@ -195,7 +195,7 @@ export async function importFromDatabase( indexes: {}, foreignKeys: {}, uniqueConstraints: {}, - checkConstraint: {}, + checkConstraints: {}, relations: [], schema, }; @@ -414,7 +414,7 @@ AND const tableInResult = result[tableName]; // if (typeof tableInResult === 'undefined') continue; - tableInResult.checkConstraint[constraintName] = { + tableInResult.checkConstraints[constraintName] = { name: constraintName, value: constraintValue, }; diff --git a/packages/api/src/mysql/schema.ts b/packages/api/src/mysql/schema.ts index 4b8c862..ec60c7c 100644 --- a/packages/api/src/mysql/schema.ts +++ b/packages/api/src/mysql/schema.ts @@ -95,7 +95,7 @@ const table = object({ foreignKeys: record(string(), fk), compositePrimaryKeys: record(string(), compositePK), uniqueConstraints: record(string(), uniqueConstraint).default({}), - checkConstraint: record(string(), checkConstraint).default({}), + checkConstraints: record(string(), checkConstraint).default({}), /* lab extension */ schema: string(), relations: array(relation).default([]), @@ -411,7 +411,7 @@ export const squashSnapshot = (json: Snapshot): SnapshotSquashed => { ); const squashedCheckConstraints = mapValues( - it[1].checkConstraint, + it[1].checkConstraints, (check) => { return MySqlSquasher.squashCheck(check); }, diff --git a/packages/api/src/mysql/serializer/snapshot.ts b/packages/api/src/mysql/serializer/snapshot.ts index cab9728..0ff7f78 100644 --- a/packages/api/src/mysql/serializer/snapshot.ts +++ b/packages/api/src/mysql/serializer/snapshot.ts @@ -412,7 +412,7 @@ export function drizzleObjectsToSnapshot( foreignKeys: foreignKeysObject, compositePrimaryKeys: primaryKeysObject, uniqueConstraints: uniqueConstraintObject, - checkConstraint: checkConstraintObject, + checkConstraints: checkConstraintObject, /* lab extension */ description, relations: tableRelations, diff --git a/packages/api/src/mysql/serializer/typescript.ts b/packages/api/src/mysql/serializer/typescript.ts index 2d0fc7f..0a87d9e 100644 --- a/packages/api/src/mysql/serializer/typescript.ts +++ b/packages/api/src/mysql/serializer/typescript.ts @@ -158,7 +158,9 @@ export function snapshotToTypeScript(snapshot: Snapshot, casing: Casing) { const uniqueImports = Object.values(it.uniqueConstraints).map( () => "unique", ); - const checkImports = Object.values(it.checkConstraint).map(() => "check"); + const checkImports = Object.values(it.checkConstraints).map( + () => "check", + ); res.mysql.push(...idxImports); res.mysql.push(...fkImpots); @@ -267,7 +269,7 @@ export function snapshotToTypeScript(snapshot: Snapshot, casing: Casing) { filteredFKs.length > 0 || Object.keys(table.compositePrimaryKeys).length > 0 || Object.keys(table.uniqueConstraints).length > 0 || - Object.keys(table.checkConstraint).length > 0 + Object.keys(table.checkConstraints).length > 0 ) { statement += ",\n"; statement += "(table) => {\n"; @@ -287,7 +289,7 @@ export function snapshotToTypeScript(snapshot: Snapshot, casing: Casing) { withCasing, ); statement += createTableChecks( - Object.values(table.checkConstraint), + Object.values(table.checkConstraints), withCasing, ); statement += "\t}\n"; diff --git a/packages/visualizer/src/compute.ts b/packages/visualizer/src/compute.ts index dc79884..1f0f739 100644 --- a/packages/visualizer/src/compute.ts +++ b/packages/visualizer/src/compute.ts @@ -15,20 +15,25 @@ export type Snapshot = PgSnapshot | SQLiteSnapshot | MySqlSnapshot; type CompositePrimaryKeyDefinition = | PgSnapshot["tables"][number]["compositePrimaryKeys"][number] - | SQLiteSnapshot["tables"][number]["compositePrimaryKeys"][number]; + | SQLiteSnapshot["tables"][number]["compositePrimaryKeys"][number] + | MySqlSnapshot["tables"][number]["compositePrimaryKeys"][number]; type RelationDefinition = | PgSnapshot["tables"][number]["relations"][number] - | SQLiteSnapshot["tables"][number]["relations"][number]; + | SQLiteSnapshot["tables"][number]["relations"][number] + | MySqlSnapshot["tables"][number]["relations"][number]; type CheckDefinition = | PgSnapshot["tables"][number]["checkConstraints"][number] - | SQLiteSnapshot["tables"][number]["checkConstraints"][number]; + | SQLiteSnapshot["tables"][number]["checkConstraints"][number] + | MySqlSnapshot["tables"][number]["checkConstraints"][number]; type PolicyDefinition = PgSnapshot["policies"][number]; type UniqueConstraintDefinition = | PgSnapshot["tables"][number]["uniqueConstraints"][number] - | SQLiteSnapshot["tables"][number]["uniqueConstraints"][number]; + | SQLiteSnapshot["tables"][number]["uniqueConstraints"][number] + | MySqlSnapshot["tables"][number]["uniqueConstraints"][number]; type IndexDefinition = | PgSnapshot["tables"][number]["indexes"][number] - | SQLiteSnapshot["tables"][number]["indexes"][number]; + | SQLiteSnapshot["tables"][number]["indexes"][number] + | MySqlSnapshot["tables"][number]["indexes"][number]; type ForeignKeyDefinition = { id: string; fkName: string; @@ -452,6 +457,152 @@ export async function compute(snapshot: Snapshot) { break; } + case "mysql": { + /* Tables */ + Object.values(snapshot.tables).forEach((table) => { + /* Foreign Keys */ + const foreignKeys = Object.values(table.foreignKeys).flatMap((fk) => { + const fkName = fk.name; + const tableTo = fk.tableTo; + const tableFrom = table.name; + const onDelete = fk.onDelete; + const onUpdate = fk.onUpdate; + + return fk.columnsFrom.map((columnFrom, colIdx) => { + const columnTo = fk.columnsTo[colIdx]; + + return { + id: `${fkName}_${colIdx}`, + fkName, + tableFrom, + columnFrom, + columnTo, + tableTo, + onDelete, + onUpdate, + }; + }); + }); + + foreignKeys.forEach((foreignKey) => { + edges.push({ + id: foreignKey.id, + source: foreignKey.tableTo, + sourceHandle: `${foreignKey.columnTo}-right`, + target: foreignKey.tableFrom, + targetHandle: `${foreignKey.columnFrom}-left`, + style: { strokeWidth: 2 }, + type: "smoothstep", + }); + }); + + /* Composite Primary Keys */ + const compositePrimaryKeys = Object.values(table.compositePrimaryKeys); + + /* Relations */ + const relations = table.relations; + + relations.forEach((relation) => { + edges.push({ + id: crypto.randomUUID(), + source: relation.referencedTableName, + sourceHandle: "relation", + target: table.name, + targetHandle: relation.fieldName, + style: { strokeWidth: 2, strokeDasharray: "5" }, + type: "smoothstep", + }); + }); + + /* Checks */ + const checks = Object.values(table.checkConstraints); + + /* Unique Constraints */ + const uniqueConstraints = Object.values(table.uniqueConstraints); + + /* Indexes */ + const indexes = Object.values(table.indexes); + + /* Nodes */ + nodes.push({ + id: table.name, + data: { + name: table.name, + description: table.description, + schema: table.schema, + columns: Object.values(table.columns).map((column) => { + const foreignKey = foreignKeys.find( + (fk) => fk.columnFrom === column.name, + ); + + return { + name: column.name, + dataType: column.type, + description: column.description, + isPrimaryKey: + column.primaryKey || + compositePrimaryKeys.some((cpk) => + cpk.columns.includes(column.name), + ), + isForeignKey: Boolean(foreignKey), + isNotNull: column.notNull, + isUnique: false, + default: column.default, + defaultFn: column.defaultFn, + enumValues: column.enumValues, + jsonShape: column.jsonShape, + onDelete: foreignKey?.onDelete, + onUpdate: foreignKey?.onUpdate, + }; + }), + isRLSEnabled: false, + provider: undefined, + checks, + relations, + policies: [], + compositePrimaryKeys, + foreignKeys, + uniqueConstraints, + indexes, + }, + position: { x: 0, y: 0 }, + type: "table", + }); + }); + + /* Views */ + Object.values(snapshot.views).forEach((view) => { + nodes.push({ + id: view.name, + data: { + name: view.name, + schema: view.schema, + definition: view.definition, + description: view.description, + columns: Object.values(view.columns).map((column) => { + return { + name: column.name, + dataType: column.type, + isPrimaryKey: column.primaryKey, + isNotNull: column.notNull, + isUnique: false, + default: column.default, + defaultFn: column.defaultFn, + enumValues: column.enumValues, + description: column.description, + }; + }), + materialized: false, + with: undefined, + isExisting: view.isExisting, + provider: undefined, + }, + position: { x: 0, y: 0 }, + type: "view", + }); + }); + break; + } } return getLayoutedElements(nodes, edges); diff --git a/vscode-extension/CHANGELOG.md b/vscode-extension/CHANGELOG.md index dfebf0c..9d7bd28 100644 --- a/vscode-extension/CHANGELOG.md +++ b/vscode-extension/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to the "drizzle-orm" extension will be documented in this fi Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +## [0.7.0] + +- Add support for MySQL +- Fix code lens showing up in non-drizzle-kit config files + ## [0.6.0] - Add Drizzle Studio command diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json index 5f2201c..5960940 100644 --- a/vscode-extension/package-lock.json +++ b/vscode-extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-drizzle-orm", - "version": "0.6.0", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-drizzle-orm", - "version": "0.6.0", + "version": "0.7.0", "license": "MIT", "devDependencies": { "@types/mocha": "^10.0.9", diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 0db4892..65f5899 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -3,7 +3,7 @@ "displayName": "Drizzle ORM", "description": "Adds schema visualizer for Drizzle ORM", "preview": true, - "version": "0.6.0", + "version": "0.7.0", "private": true, "icon": "icon.png", "license": "MIT", diff --git a/vscode-extension/src/utils.ts b/vscode-extension/src/utils.ts index 0a9949e..32b7016 100644 --- a/vscode-extension/src/utils.ts +++ b/vscode-extension/src/utils.ts @@ -88,9 +88,7 @@ export function findDrizzleConfigLines( (text.includes("defineConfig") || text.includes("type Config") || text.includes("satisfies Config")) && - options.requireDb - ? text.includes("dbCredentials") - : true; + (options.requireDb ? text.includes("dbCredentials") : true); if (!isConfig) { return [];