Skip to content

Commit

Permalink
feat: (enhancement) Seeds and Scripts (#239)
Browse files Browse the repository at this point in the history
* init seeds:
- Updates types for Profile relation
- Adds initial structure for dev/prod seeds

* seeds and scripts:
- Adds example for users
- Adds Readme
- Adds Example Script
- Adds Script Runner
- updates pacakage json
- adds scripts for seed:prod and run:script

* lint

* missing ,

* pr feedback
  • Loading branch information
mthomps4 authored Nov 16, 2021
1 parent 3ebb437 commit 1714340
Show file tree
Hide file tree
Showing 12 changed files with 260 additions and 11 deletions.
1 change: 1 addition & 0 deletions packages/create-bison-app/.tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodejs 16.5.0
3 changes: 2 additions & 1 deletion packages/create-bison-app/template/_.env.local.ejs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# ENV vars here override .env when running locally
# DATABASE_URL="postgresql://postgres@localhost:5432/<%= name -%>_dev?schema=public"
APP_SECRET=foo
APP_SECRET=foo
APP_ENV=development
19 changes: 18 additions & 1 deletion packages/create-bison-app/template/graphql/modules/profile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { objectType } from 'nexus';
import { objectType, inputObjectType } from 'nexus';

// Profile Type
export const Profile = objectType({
Expand Down Expand Up @@ -29,3 +29,20 @@ export const Profile = objectType({
});
},
});

export const ProfileCreateInput = inputObjectType({
name: 'ProfileCreateInput',
description: 'Profile Input for relational Create',
definition(t) {
t.nonNull.string('firstName');
t.nonNull.string('lastName');
},
});

export const ProfileRelationalCreateInput = inputObjectType({
name: 'ProfileRelationalCreateInput',
description: 'Input to Add a new user',
definition(t) {
t.nonNull.field('create', { type: 'ProfileCreateInput' });
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -256,5 +256,8 @@ export const UserCreateInput = inputObjectType({
t.field('roles', {
type: list('Role'),
});
t.field('profile', {
type: 'ProfileRelationalCreateInput'
})
},
});
3 changes: 3 additions & 0 deletions packages/create-bison-app/template/package.json.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"db:deploy": "yarn prisma migrate deploy",
"db:reset": "yarn prisma migrate reset",
"db:seed": "yarn prisma db seed",
"db:seed:prod": "cross-env APP_ENV=production prisma db seed",
"db:setup": "yarn db:reset",
"dev": "concurrently -n \"WATCHERS,NEXT\" -c \"black.bgYellow.dim,black.bgCyan.dim\" \"yarn watch:all\" \"next dev\"",
"dev:typecheck": "tsc --noEmit",
Expand All @@ -32,6 +33,7 @@
"g:test:request": "hygen test request --name",
"g:test:util": "hygen test util --name",
"lint": "yarn eslint . --ext .ts,.tsx --fix --ignore-pattern tmp",
"run:script": "yarn ts-node prisma/scripts/run.ts -f",
"setup:dev": "yarn db:setup && yarn build:types",
"start": "next start -p $PORT",
"test": "yarn withEnv:test jest --runInBand --watch",
Expand All @@ -56,6 +58,7 @@
"@sendgrid/mail": "^7.4.4",
"apollo-server-micro": "^3.1.1",
"bcryptjs": "^2.4.3",
"commander": "^8.1.0",
"cross-fetch": "3.0.5",
"framer-motion": "^4",
"graphql": "^15.5.0",
Expand Down
66 changes: 66 additions & 0 deletions packages/create-bison-app/template/prisma/SeedsAndScripts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Seeds and Scripts

## Seeds

### Dev VS Prod

- `yarn db:seed` (DEV)
- `yarn db:seed:prod` (PROD)

We use `APP_ENV` in the scripts to determine the dataset returned for seeds.
This ENV is set in your .env.local file as well, and can be manually set as an ENV in your deploy environment if needed for other scripts.

### File Breakdown (Seeds)

```sh
/seeds
--/model
----/data.ts
----/prismaRunner.ts
----/index.ts
```

**data.ts** contains the exported function `seedModelData: ModelCreateInput[]` this function leverages APP_ENV to return the dataset expected for Dev vs Prod. In the case of `users` this returns `initialDevUsers: UserCreateInput[]` or `initalProdUsers: UserCreateInput[]`.

**prismaRunner.ts** this file contains the Prisma `UPSERT` call for the model. We leverage upsert here so that seeds can potentially be ran more than once as your models and data expand over time.

**index.ts** export of functions for main seed file

### Relationships

In the event your model has a relationship dependency ie. Accounts, Organizations, etc. The prismaRunners are set to return a `Pick<Model, 'id'>` result for you to leverage in future seeds. The dependent runner would expand to take these parameters.

**User/Organization example:**

```ts
import {orgSeedData, seedOrganizations } from './seeds/organizations';
import {userSeedData, seedUsers } from './seeds/users';

const [{ id: orgId }] = await seedOrganizations(orgSeedData);
await seedUsers(userSeedData(orgId));
```

```ts
const initialDevUsers = (orgId: string): UserCreateInput[] => [
{
email: 'john.doe@echobind.com',
organization: { connect: { id: orgId } },
// ...other create data
}
]
```

## Scripts

### File Breakdown (Scripts)

```sh
/scripts
--/run.ts
--/exampleUserScript.ts
```

**run.ts** This is the main run file leveraged by `yarn run:script {filename}`.
This script takes a file name to be run via `commander`. These scripts can be ANYTHING. However, in our example, we've leveraged the same `seedUsers` runner to add new employees.

**exampleUserScript.ts** This is an example script file that can be ran with `yarn run:script exampleUserScript.ts`. This script leverages the same `seedUsers` prisma runner to add a few new employees to the team.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { hashPassword } from '../../services/auth';
import { Role, UserCreateInput } from '../../types';

import { seedUsers } from '../seeds/users';

// HR: Hey, we've had a few more employees join -- can you create an account for them?!

const INITIAL_PASSWORD = 'test1234';

const main = async () => {
const newEmployees: UserCreateInput[] = [
{
profile: {
create: {
firstName: 'Cisco',
lastName: 'Ramon',
},
},
email: 'cisco.ramon@speedforce.net',
password: hashPassword(INITIAL_PASSWORD),
roles: [Role.ADMIN],
},
{
profile: {
create: {
firstName: 'Caitlin',
lastName: 'Snow',
},
},
email: 'caitlin.snow@speedforce.net',
password: hashPassword(INITIAL_PASSWORD),
roles: [Role.ADMIN],
},
{
profile: {
create: {
firstName: 'Harrison',
lastName: 'Wells',
},
},
email: 'harrison.wells@speedforce.net',
password: hashPassword(INITIAL_PASSWORD),
roles: [Role.ADMIN],
},
];

await seedUsers(newEmployees);
};

main()
.catch((e) => console.error(e))
.finally(async () => {
await prisma.$disconnect();
});
23 changes: 23 additions & 0 deletions packages/create-bison-app/template/prisma/scripts/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Command } from 'commander';
import { spawn } from 'child_process';

const program = new Command();
const run = () => {
program.option('-f, --file <type>', 'filename of script to run');

program.parse(process.argv);

const { file } = program.opts();
console.log({ file });

const child = spawn(`yarn ts-node ${__dirname}/${file}`, {
shell: true,
stdio: 'inherit',
});

child.on('exit', function (code) {
process.exit(code);
});
};

run();
18 changes: 9 additions & 9 deletions packages/create-bison-app/template/prisma/seed.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// import { UserFactory } from '../tests/factories';
// import { Role } from '../types';
// import { prisma } from '../lib/prisma';
import { userSeedData, seedUsers } from './seeds/users';

export async function seed() {
// Add seeds here. You can use factories or raw prisma.create/upsert calls.
console.log('no seeds yet!');
// await UserFactory.create({ email: 'hello@wee.net' });
// await UserFactory.create({ roles: [Role.ADMIN] });
}
const seed = async () => {
console.log('seeding Users...');
await seedUsers(userSeedData);
};

seed()
.catch((e) => console.error(e))
.finally(() => console.log('Seeding Complete'));
51 changes: 51 additions & 0 deletions packages/create-bison-app/template/prisma/seeds/users/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { hashPassword } from '../../../services/auth';
import { Role, UserCreateInput } from '../../../types';

// *********************************************
// ** DEVELOPMENT DATA SET
// *********************************************

const INITIAL_PASSWORD = 'test1234';

const initialDevUsers: UserCreateInput[] = [
{
email: 'barry.allen@speedforce.net',
password: hashPassword(INITIAL_PASSWORD),
roles: [Role.ADMIN],
profile: {
create: {
firstName: 'Barry',
lastName: 'Allen',
},
},
},
];

// *********************************************
// ** PRODUCTION DATA SET
// *********************************************

const INITIAL_PROD_PASSWORD = 'strong@password';

const initialProdUsers: UserCreateInput[] = [
{
email: 'apps@echobind.com',
password: hashPassword(INITIAL_PROD_PASSWORD),
roles: [Role.ADMIN],
profile: {
create: {
firstName: 'EB',
lastName: 'Admin',
},
},
},
];

// *********************************************
// ** MAIN DATA EXPORT
// *********************************************

const appEnv = process.env.APP_ENV || 'development';

export const userSeedData: UserCreateInput[] =
appEnv === 'production' ? initialProdUsers : initialDevUsers;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './data';
export * from './prismaRunner';
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { User, UserCreateInput } from '../../../types';
import { prisma } from '../../../lib/prisma';

type SeedUserResult = Pick<User, 'id' | 'email'>;

export const seedUsers = async (users: UserCreateInput[]): Promise<SeedUserResult[]> => {
const userPromiseArray = users.map(
async (user): Promise<SeedUserResult> =>
prisma.user.upsert({
where: {
email: user.email,
},
create: {
email: user.email,
password: user.password,
roles: user.roles,
profile: user.profile,
},
update: {},
select: {
id: true,
email: true,
},
})
);

return Promise.all(userPromiseArray);
};

0 comments on commit 1714340

Please sign in to comment.