Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: (enhancement) Seeds and Scripts #239

Merged
merged 5 commits into from
Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: '[email protected]',
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: '[email protected]',
password: hashPassword(INITIAL_PASSWORD),
roles: [Role.ADMIN],
},
{
profile: {
create: {
firstName: 'Caitlin',
lastName: 'Snow',
},
},
email: '[email protected]',
password: hashPassword(INITIAL_PASSWORD),
roles: [Role.ADMIN],
},
{
profile: {
create: {
firstName: 'Harrison',
lastName: 'Wells',
},
},
email: '[email protected]',
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: '[email protected]' });
// 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: '[email protected]',
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: '[email protected]',
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);
};