Skip to content

Commit 7c8dc28

Browse files
committed
feat: adds helpers package
1 parent dd289bf commit 7c8dc28

File tree

16 files changed

+303
-648
lines changed

16 files changed

+303
-648
lines changed

germinator-cli/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"prepublishOnly": "yarn clean && yarn build && yarn build:es"
3535
},
3636
"dependencies": {
37-
"@germinator/core": "0.4",
37+
"@germinator/helpers": "0.4",
3838
"@germinator/node": "0.4",
3939
"knex": "0.21",
4040
"yargs": "16"

germinator-cli/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import yargs from 'yargs';
22
import { runSeeds } from '@germinator/node';
3+
import { makeHelpers } from '@germinator/helpers';
34

45
type SubcommandOptions<
56
Options extends { [name: string]: yargs.Options },
@@ -132,6 +133,7 @@ function buildCLI() {
132133
}
133134

134135
await runSeeds({
136+
helpers: makeHelpers(),
135137
folder: opts.folder!,
136138
db: {
137139
client: opts.client,

germinator-cli/tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"include": ["src/**/*"],
88
"exclude": ["node_modules"],
99
"references": [
10-
{ "path": "../germinator-core" },
10+
{ "path": "../germinator-helpers" },
1111
{ "path": "../germinator-node" }
1212
]
1313
}

germinator-core/package.json

-11
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,13 @@
3131
"prepublishOnly": "yarn clean && yarn build && yarn build:es"
3232
},
3333
"dependencies": {
34-
"@lcdev/app-config": "^2.0.0-rc.10",
3534
"@lcdev/mapper": "0.1",
3635
"ajv": "7",
37-
"bcrypt": "5",
38-
"chance": "1",
3936
"debug": "4",
40-
"faker": "4",
4137
"handlebars": "4",
42-
"handlebars-helper-repeat-root-fixed": "2",
43-
"handlebars-helpers": "0.10",
4438
"hogan.js": "3",
4539
"js-yaml": "4",
4640
"json-stable-stringify": "1",
47-
"lodash.get": "4",
4841
"object-hash": "1",
4942
"p-limit": "3",
5043
"to-snake-case": "1"
@@ -53,15 +46,11 @@
5346
"knex": "0.21"
5447
},
5548
"devDependencies": {
56-
"@types/bcrypt": "3",
57-
"@types/chance": "1",
5849
"@types/debug": "4",
59-
"@types/faker": "4",
6050
"@types/fs-extra": "9",
6151
"@types/hogan.js": "3",
6252
"@types/js-yaml": "4",
6353
"@types/json-stable-stringify": "1",
64-
"@types/lodash.get": "4",
6554
"@types/object-hash": "1",
6655
"@types/tmp": "0.2",
6756
"@types/to-snake-case": "1",

germinator-core/src/errors.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
export class InvalidSeed extends Error {}
2-
export class InvalidSeedEntryCreation extends Error {}
3-
export class UpdateOfDeletedEntry extends Error {}
4-
export class UpdateOfMultipleEntries extends Error {}
1+
export class GerminatorError extends Error {}
2+
export class InvalidSeed extends GerminatorError {}
3+
export class InvalidSeedEntryCreation extends GerminatorError {}
4+
export class UpdateOfDeletedEntry extends GerminatorError {}
5+
export class UpdateOfMultipleEntries extends GerminatorError {}

germinator-core/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { setupDatabase } from './database';
2-
export { renderSeed, renderTemplate } from './template';
2+
export { renderSeed, renderTemplate, Helpers } from './template';
33
export { SeedEntry, SeedFile, resolveAllEntries } from './seeds';
4+
export { GerminatorError } from './errors';

germinator-core/src/template.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { InvalidSeed } from './errors';
55

66
type Obj = Record<string, any>;
77
type AnyFunction<Args extends any[] = any[], Ret = any> = (...args: Args) => Ret;
8-
type Helpers = Record<string, AnyFunction>;
8+
9+
export type Helpers = Record<string, AnyFunction>;
910

1011
export function renderTemplate(contents: string, data: Obj, helpers: Helpers): string {
1112
// any date objects, which YAML has first-class support for, need to be stringified when rendering

germinator-helpers/.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('@lcdev/eslint-config/cwd')(__dirname);

germinator-helpers/package.json

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "@germinator/helpers",
3+
"description": "Dynamic database seeds using declarative YAML files",
4+
"version": "0.4.0",
5+
"license": "MPL-2.0",
6+
"author": {
7+
"name": "Launchcode",
8+
"email": "admin@lc.dev",
9+
"url": "https://lc.dev"
10+
},
11+
"repository": {
12+
"type": "git",
13+
"url": "https://github.com/launchcodedev/germinator.git"
14+
},
15+
"main": "dist/index.js",
16+
"module": "dist/es/index.js",
17+
"types": "dist/index.d.ts",
18+
"files": [
19+
"/dist",
20+
"!*.tsbuildinfo",
21+
"!*.test.*"
22+
],
23+
"scripts": {
24+
"build": "tsc -b",
25+
"build:es": "tsc -b tsconfig.es.json",
26+
"clean": "rm -rf dist *.tsbuildinfo",
27+
"lint": "eslint src",
28+
"fix": "eslint --fix src",
29+
"test": "jest",
30+
"prepublishOnly": "yarn clean && yarn build && yarn build:es"
31+
},
32+
"dependencies": {
33+
"@germinator/core": "0.4",
34+
"bcrypt": "5",
35+
"chance": "1",
36+
"faker": "4",
37+
"handlebars-helper-repeat-root-fixed": "2",
38+
"handlebars-helpers": "0.10",
39+
"lodash.get": "4"
40+
},
41+
"devDependencies": {
42+
"@types/bcrypt": "3",
43+
"@types/chance": "1",
44+
"@types/faker": "4",
45+
"@types/lodash.get": "4"
46+
},
47+
"prettier": "@lcdev/prettier",
48+
"jest": {
49+
"preset": "@lcdev/jest"
50+
}
51+
}

germinator-helpers/src/index.ts

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { renderTemplate, GerminatorError, Helpers } from '@germinator/core';
2+
import * as Handlebars from 'handlebars';
3+
import * as YAML from 'js-yaml';
4+
import * as faker from 'faker';
5+
import { Chance } from 'chance';
6+
import * as bcrypt from 'bcrypt';
7+
import moment from 'moment';
8+
import get from 'lodash.get';
9+
import handlebarHelpers from 'handlebars-helpers';
10+
import repeatHelper from 'handlebars-helper-repeat-root-fixed';
11+
12+
// insecure password hashed cache - uses same salt for all passwords!
13+
// hashSync is really slow so this is useful for mock data
14+
// we can't provide a seed to getSalt either so we'll just hard code one
15+
const passwordCache: { [plainText: string]: string } = {};
16+
const insecurePasswordSalt = '$2b$10$lAuv4qM.z6qZwQ/WhmHvEu';
17+
18+
export function makeHelpers(chance: Chance.Chance = new Chance()): Helpers {
19+
const helpers = {
20+
...handlebarHelpers,
21+
repeat: repeatHelper,
22+
array(...args: any[]) {
23+
return args.slice(0, args.length - 1);
24+
},
25+
concat(...args: any[]) {
26+
return args.slice(0, args.length - 1).join('');
27+
},
28+
moment(
29+
date?: string | Date,
30+
...args: (
31+
| string
32+
| {
33+
hash: {
34+
format?: string;
35+
utc?: boolean;
36+
[op: string]: string | any;
37+
};
38+
data: {
39+
root: any;
40+
[key: string]: any;
41+
};
42+
}
43+
)[]
44+
) {
45+
if (date && date instanceof Date) {
46+
date = date.toISOString();
47+
}
48+
49+
if (!date || typeof date !== 'string') {
50+
throw new GerminatorError('moment helper requires a date {{moment date}}');
51+
}
52+
53+
const [ctx] = args.splice(-1, 1);
54+
const hash = (ctx && typeof ctx !== 'string' && ctx.hash) || {};
55+
const data = (ctx && typeof ctx !== 'string' && ctx.data) || { root: {} };
56+
57+
const { format, utc } = hash;
58+
59+
let value = utc ? moment.utc(date) : moment(date);
60+
61+
for (const arg of args) {
62+
if (!arg || typeof arg !== 'string') {
63+
throw new GerminatorError(
64+
'moment helper received an undefined argument (did you forget to quote an operation? "[add,days,5]")',
65+
);
66+
}
67+
68+
try {
69+
let op = YAML.load(
70+
// we actually render this string, so that "[add,{{x}},days]" is convenient
71+
renderTemplate(arg, { ...data.root, ...data, root: undefined }, helpers),
72+
);
73+
74+
if (!Array.isArray(op)) {
75+
// {{moment "utcdate" utc=true local}}
76+
op = [op];
77+
}
78+
79+
const [method, ...methodArgs] = op as any[];
80+
81+
if (!(value as any)[method]) {
82+
throw new GerminatorError(`no such moment operation existed (${method})`);
83+
}
84+
85+
value = (value as any)[method](...methodArgs);
86+
} catch (err) {
87+
throw new GerminatorError(`failed to parse "${arg}" moment helper argument`);
88+
}
89+
}
90+
91+
return format ? value.format(format) : value.toISOString();
92+
},
93+
momentAdd(count: number, period: string) {
94+
if (count === undefined || typeof count !== 'number') {
95+
throw new GerminatorError('momentAdd helper requires {{momentAdd 5 "days"}}');
96+
}
97+
98+
if (period === undefined || typeof period !== 'string') {
99+
throw new GerminatorError('momentAdd helper requires {{momentAdd 5 "days"}}');
100+
}
101+
102+
return `[add,${count},${period}]`;
103+
},
104+
momentSubtract(count: number, period: string) {
105+
if (!count || typeof count !== 'number') {
106+
throw new GerminatorError('momentSubtract helper requires {{momentSubtract 5 "days"}}');
107+
}
108+
109+
if (!period || typeof period !== 'string') {
110+
throw new GerminatorError('momentSubtract helper requires {{momentSubtract 5 "days"}}');
111+
}
112+
113+
return `[subtract,${count},${period}]`;
114+
},
115+
password(password?: string, ctx?: { hash: { rounds?: number; insecure?: boolean } }) {
116+
if (!password || typeof password !== 'string') {
117+
throw new GerminatorError('password helper requires password {{password "pwd"}}');
118+
}
119+
120+
const rounds = ctx?.hash?.rounds ?? 10;
121+
const insecure = ctx?.hash?.insecure;
122+
123+
if (insecure) {
124+
if (!passwordCache[password]) {
125+
passwordCache[password] = bcrypt.hashSync(password, insecurePasswordSalt);
126+
}
127+
128+
return passwordCache[password];
129+
}
130+
131+
return bcrypt.hashSync(password, rounds);
132+
},
133+
faker(name: string | object | undefined, ctx?: { hash: any }) {
134+
if (!name || typeof name === 'object') {
135+
throw new GerminatorError('faker helper requires data type {{faker "email"}}');
136+
}
137+
138+
const fn = get(faker, name);
139+
140+
if (!fn) {
141+
throw new GerminatorError(`${name} is not a valid faker.js value type`);
142+
}
143+
144+
return fn(ctx && Object.keys(ctx.hash).length > 0 ? { ...ctx.hash } : undefined);
145+
},
146+
chance(name: string, ...args: any[]) {
147+
if (!name || typeof name === 'object') {
148+
throw new GerminatorError('chance helper requires data type {{chance "email"}}');
149+
}
150+
151+
const fn = (chance as any)[name];
152+
153+
if (!fn) {
154+
throw new GerminatorError(`${name} is not a valid chance value type`);
155+
}
156+
157+
const [ctx]: { hash: any }[] = args;
158+
159+
// If the first argument is not the chance context (containing hash),
160+
// assume we are passing non-object args to chance (eg. array or scalar)
161+
if (!ctx.hash) {
162+
return fn.call(chance, ...args.slice(0, args.length - 1));
163+
}
164+
165+
if (name === 'date' && ctx) {
166+
const { min, max, ...opts } = ctx.hash;
167+
const minDate = min !== undefined && new Date(min);
168+
const maxDate = max !== undefined && new Date(max);
169+
170+
// we'll help out by toISOString here
171+
const date = moment.utc(chance.date({ ...opts, min: minDate, max: maxDate }));
172+
173+
return date.toISOString();
174+
}
175+
176+
return fn.call(chance, ctx ? { ...ctx.hash } : {});
177+
},
178+
};
179+
180+
return helpers;
181+
}

germinator-helpers/src/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
declare module 'handlebars-helpers';
2+
declare module 'handlebars-helper-repeat-root-fixed';

germinator-helpers/tsconfig.es.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"target": "es2020",
5+
"module": "es2020",
6+
"outDir": "./dist/es"
7+
}
8+
}

germinator-helpers/tsconfig.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "@lcdev/tsconfig",
3+
"compilerOptions": {
4+
"rootDir": "./src",
5+
"outDir": "./dist"
6+
},
7+
"include": ["src/**/*"],
8+
"exclude": ["node_modules"],
9+
"references": [
10+
{ "path": "../germinator-core" }
11+
]
12+
}

0 commit comments

Comments
 (0)