Skip to content

Commit

Permalink
fix(env): load and parse database url in runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
Victor Korzunin committed Aug 21, 2021
1 parent d6e3c33 commit efe8b97
Show file tree
Hide file tree
Showing 11 changed files with 458 additions and 55 deletions.
50 changes: 24 additions & 26 deletions .plop/index.ts.hbs
Original file line number Diff line number Diff line change
@@ -1,43 +1,41 @@
import { ModelCtor, Sequelize } from 'sequelize';
import { tryLoadEnvs } from '@prisma/sdk';
import path from 'path';
import { findSync, parseDatabaseUrl } from './utils';

const dirname = findSync(process.cwd(), ['{{relativeOutputDir}}', '{{slsRelativeOutputDir}}'], ['d'], ['d'], 1)[0] || __dirname;

import config from '../config/config.json';
{{#each models}}
import { {{name}}Factory } from './{{name}}';
{{/each}}

const env = process.env.NODE_ENV != 'prd' ? 'development' : 'production';
const {username, password, database, host, port} = config[env];
const config = {{{config}}};

const loadedEnv = tryLoadEnvs({
rootEnvPath: config.relativeEnvPaths.rootEnvPath && path.resolve(dirname, config.relativeEnvPaths.rootEnvPath),
schemaEnvPath: config.relativeEnvPaths.schemaEnvPath && path.resolve(dirname, config.relativeEnvPaths.schemaEnvPath),
});
const env = loadedEnv ? loadedEnv.parsed : {};
const databaseUrl = config.datasource.url.fromEnvVar
? env[config.datasource.url.fromEnvVar]
: config.datasource.url.value;
const { driver, user, password, host, port, database } = parseDatabaseUrl(databaseUrl);

export const createInstance = async () => {
const sequelize = new Sequelize(
database,
username,
password,
{
host,
port,
ssl: true,
dialect: 'postgres',
dialectModule: pg,
pool: {},
dialectOptions: {
connectTimeout: process.env.CONNECTION_TIMEOUT
},
define: {
freezeTableName: true,
timestamps: true,
paranoid: true
}
},
);
const sequelize = new Sequelize(database, user, password, {
host,
port,
ssl: true,
dialect: driver,
});

const models = {
{{#each models}}
{{name}}: {{name}}Factory(sequelize),
{{/each}}
};

Object.keys(models).forEach(model => {
Object.keys(models).forEach((model) => {
if (models[model].associate) {
models[model].associate(models);
}
Expand All @@ -51,6 +49,6 @@ export const createInstance = async () => {

return {
sequelize,
models
models,
};
};
20 changes: 20 additions & 0 deletions .plop/plopfile.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
const path = require('path');

module.exports = function (plop) {
plop.setGenerator('utils', {
actions: () => [
{
type: 'add',
path: 'utils/find.ts',
templateFile: path.join(__dirname, './utils/find.ts.hbs'),
},
{
type: 'add',
path: 'utils/parseDatabaseUrl.ts',
templateFile: path.join(__dirname, './utils/parseDatabaseUrl.ts.hbs'),
},
{
type: 'add',
path: 'utils/index.ts',
templateFile: path.join(__dirname, './utils/index.ts.hbs'),
},
],
});

plop.setGenerator('index.ts', {
actions: () => [
{
Expand Down
112 changes: 112 additions & 0 deletions .plop/utils/find.ts.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import fs from 'fs';
import path from 'path';

type ItemType = 'd' | 'f' | 'l';
type Handler = (base: string, item: string, type: ItemType) => boolean | string;

/**
* Transform a dirent to a file type
* @param dirent
* @returns
*/
function direntToType(dirent: fs.Dirent | fs.Stats) {
return dirent.isFile() ? 'f' : dirent.isDirectory() ? 'd' : dirent.isSymbolicLink() ? 'l' : undefined;
}

/**
* Is true if at least one matched
* @param string to match aigainst
* @param regexs to be matched with
* @returns
*/
function isMatched(string: string, regexs: (RegExp | string)[]) {
for (const regex of regexs) {
if (typeof regex === 'string') {
if (string.includes(regex)) {
return true;
}
} else if (regex.exec(string)) {
return true;
}
}

return false;
}

/**
* Find paths that match a set of regexes
* @param root to start from
* @param match to match against
* @param types to select files, folders, links
* @param deep to recurse in the directory tree
* @param limit to limit the results
* @param handler to further filter results
* @param found to add to already found
* @param seen to add to already seen
* @returns found paths (symlinks preserved)
*/
export function findSync(
root: string,
match: (RegExp | string)[],
types: ('f' | 'd' | 'l')[] = ['f', 'd', 'l'],
deep: ('d' | 'l')[] = [],
limit: number = Infinity,
handler: Handler = () => true,
found: string[] = [],
seen: Record<string, true> = {}
) {
try {
const realRoot = fs.realpathSync(root);

// we make sure not to loop infinitely
if (seen[realRoot]) {
return found;
}

// we stop if we found enough results
if (limit - found.length <= 0) {
return found;
}

// we check that the root is a directory
if (direntToType(fs.statSync(realRoot)) !== 'd') {
return found;
}

// we list the items in the current root
const items = fs.readdirSync(root, { withFileTypes: true });

//seen[realRoot] = true
for (const item of items) {
// we get the file info for each item
const itemName = item.name;
const itemType = direntToType(item);
const itemPath = path.join(root, item.name);

// if the item is one of the selected
if (itemType && types.includes(itemType)) {
// if the path of an item has matched
if (isMatched(itemPath, match)) {
const value = handler(root, itemName, itemType);

// if we changed the path value
if (typeof value === 'string') {
found.push(value);
}
// if we kept the default path
else if (value === true) {
found.push(itemPath);
}
}
}

if (deep.includes(itemType as any)) {
// dive within the directory tree
// we recurse and continue mutating `found`
findSync(itemPath, match, types, deep, limit, handler, found, seen);
}
}
} catch {}

return found;
}
2 changes: 2 additions & 0 deletions .plop/utils/index.ts.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './find';
export * from './parseDatabaseUrl';
51 changes: 51 additions & 0 deletions .plop/utils/parseDatabaseUrl.ts.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import url from 'url';
import querystring from 'querystring';

export function parseDatabaseUrl(databaseUrl: string) {
const parsedUrl = url.parse(databaseUrl, false, true);

// Query parameters end up directly in the configuration.
const config = querystring.parse(parsedUrl.query);

config.driver = (parsedUrl.protocol || 'sqlite3:')
// The protocol coming from url.parse() has a trailing :
.replace(/\:$/, '');

// Cloud Foundry will sometimes set a 'mysql2' scheme instead of 'mysql'.
if (config.driver == 'mysql2') config.driver = 'mysql';

// url.parse() produces an "auth" that looks like "user:password". No
// individual fields, unfortunately.
if (parsedUrl.auth) {
const userPassword = parsedUrl.auth.split(':', 2);
config.user = userPassword[0];
if (userPassword.length > 1) {
config.password = userPassword[1];
}
}

if (config.driver === 'sqlite3') {
if (parsedUrl.hostname) {
if (parsedUrl.pathname) {
// Relative path.
config.filename = parsedUrl.hostname + parsedUrl.pathname;
} else {
// Just a filename.
config.filename = parsedUrl.hostname;
}
} else {
// Absolute path.
config.filename = parsedUrl.pathname;
}
} else {
// Some drivers (e.g., redis) don't have database names.
if (parsedUrl.pathname) {
config.database = parsedUrl.pathname.replace(/^\//, '').replace(/\/$/, '');
}

if (parsedUrl.hostname) config.host = parsedUrl.hostname;
if (parsedUrl.port) config.port = parsedUrl.port;
}

return config;
}
78 changes: 52 additions & 26 deletions prisma/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,67 @@
import { ModelCtor, Sequelize } from 'sequelize';
import { tryLoadEnvs } from '@prisma/sdk';
import path from 'path';
import { findSync, parseDatabaseUrl } from './utils';

const dirname = findSync(process.cwd(), ['prisma/models', 'models'], ['d'], ['d'], 1)[0] || __dirname;

import config from '../config/config.json';
import { UserFactory } from './User';
import { PostFactory } from './Post';

const env = process.env.NODE_ENV != 'prd' ? 'development' : 'production';
const {username, password, database, host, port} = config[env];
const config = {
"generator": {
"name": "models",
"provider": {
"fromEnvVar": null,
"value": "node ./dist/generator.js"
},
"output": {
"value": "/Users/victor/Projects/_own/prisma-sequelize-generator/prisma/models",
"fromEnvVar": "null"
},
"config": {},
"binaryTargets": [],
"previewFeatures": []
},
"relativeEnvPaths": {
"rootEnvPath": "../../.env",
"schemaEnvPath": "../../.env"
},
"datasource": {
"name": "db",
"provider": "postgresql",
"activeProvider": "postgresql",
"url": {
"fromEnvVar": "DATABASE_URL",
"value": null
}
}
};

const loadedEnv = tryLoadEnvs({
rootEnvPath: config.relativeEnvPaths.rootEnvPath && path.resolve(dirname, config.relativeEnvPaths.rootEnvPath),
schemaEnvPath: config.relativeEnvPaths.schemaEnvPath && path.resolve(dirname, config.relativeEnvPaths.schemaEnvPath),
});
const env = loadedEnv ? loadedEnv.parsed : {};
const databaseUrl = config.datasource.url.fromEnvVar
? env[config.datasource.url.fromEnvVar]
: config.datasource.url.value;
const { driver, user, password, host, port, database } = parseDatabaseUrl(databaseUrl);

export const createInstance = async () => {
const sequelize = new Sequelize(
database,
username,
password,
{
host,
port,
ssl: true,
dialect: 'postgres',
dialectModule: pg,
pool: {},
dialectOptions: {
connectTimeout: process.env.CONNECTION_TIMEOUT
},
define: {
freezeTableName: true,
timestamps: true,
paranoid: true
}
},
);
const sequelize = new Sequelize(database, user, password, {
host,
port,
ssl: true,
dialect: driver,
});

const models = {
User: UserFactory(sequelize),
Post: PostFactory(sequelize),
};

Object.keys(models).forEach(model => {
Object.keys(models).forEach((model) => {
if (models[model].associate) {
models[model].associate(models);
}
Expand All @@ -49,6 +75,6 @@ export const createInstance = async () => {

return {
sequelize,
models
models,
};
};
Loading

0 comments on commit efe8b97

Please sign in to comment.