├── generators
├── migrations
│ ├── changeEnumColumn
│ │ ├── attributes.hbs
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── addColumns
│ │ ├── get-row-defaults.hbs
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── dropTable
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── createTable
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── uuidNonNull
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── removeColumns
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── addEnumValues
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── makeColumnNonNull
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── makeColumnUnique
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── removeEnumValues
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── cascadeWithParent
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── removeNonNullColumn
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── renameEnum
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── addCascadeWithParent
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── makeColumnAllowNull
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── renameIndex
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── renameTable
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── renameColumn
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── renameConstraint
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── changeOnDelete
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── addUniqueConstraintIndex
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── removeUniqueConstraintIndex
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── rename-enum-value
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── addIndex
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── removeIndex
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── renameS3Files
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── bulkInsert
│ │ ├── insert.hbs
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── bulkDelete
│ │ ├── template.hbs
│ │ ├── delete.hbs
│ │ └── index.ts
│ ├── changeColumn
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── custom
│ │ └── index.ts
│ └── index.ts
├── migrationTypes
│ ├── tableColumnsMigration
│ │ ├── definition.hbs
│ │ ├── column.hbs
│ │ ├── columns.hbs
│ │ ├── constraint.hbs
│ │ ├── template.hbs
│ │ └── regexs.ts
│ ├── dbMigration
│ │ ├── header.hbs
│ │ ├── template.hbs
│ │ ├── actions.ts
│ │ └── index.ts
│ ├── tableColumnMigration
│ │ ├── template.hbs
│ │ ├── typeDefs.ts
│ │ └── index.ts
│ ├── tablesMigration
│ │ ├── tables.hbs
│ │ └── index.ts
│ ├── changeEnumAttributes
│ │ ├── template.hbs
│ │ ├── lib.ts
│ │ └── index.ts
│ ├── tableMigration
│ │ └── index.ts
│ ├── tableAssociationColumn
│ │ ├── index.ts
│ │ └── lib.ts
│ ├── tablesColumnMigration
│ │ └── index.ts
│ ├── index.ts
│ └── alterEnum
│ │ └── index.ts
├── logger.ts
├── generators
│ ├── migration-type-generator
│ │ ├── template.hbs
│ │ └── index.ts
│ ├── migration
│ │ ├── template.hbs
│ │ └── index.ts
│ └── migration-type
│ │ ├── index.ts
│ │ └── template.hbs
└── index.ts
├── .prettierignore
├── src
├── models
│ ├── migration
│ │ ├── index.ts
│ │ ├── definition.ts
│ │ └── hooks.ts
│ ├── migrationLock
│ │ ├── index.ts
│ │ └── definition.ts
│ └── index.ts
├── views
│ ├── error.hbs
│ ├── success.hbs
│ ├── index.hbs
│ └── index.ts
├── classes
│ ├── index.ts
│ └── WildebeestDb.ts
├── utils
│ ├── mkEnum.ts
│ ├── sleepPromise.ts
│ ├── pascalCase.ts
│ ├── getBackoffTime.ts
│ ├── createAttributes.ts
│ ├── escapeJson.ts
│ ├── isEnum.ts
│ ├── createHooks.ts
│ ├── clearSchema.ts
│ ├── freshIndexes.ts
│ ├── getNextNumber.ts
│ ├── logSection.ts
│ ├── tableExists.ts
│ ├── reverseMigrator.ts
│ ├── invert.ts
│ ├── getKeys.ts
│ ├── getForeignKeyName.ts
│ ├── expectedColumnNames.ts
│ ├── getAssociationColumnName.ts
│ ├── listColumns.ts
│ ├── dropEnum.ts
│ ├── restoreFromDump.ts
│ ├── indexConstraints.ts
│ ├── getAssociationAttribute.ts
│ ├── listIndexNames.ts
│ ├── columnAllowsNull.ts
│ ├── createUmzugLogger.ts
│ ├── listEnumAttributes.ts
│ ├── getColumnDefault.ts
│ ├── writeSchema.ts
│ ├── apply.ts
│ ├── createIndex.ts
│ ├── inferTableReference.ts
│ ├── getAssociationsByModelName.ts
│ ├── listConstraintNames.ts
│ ├── renameS3File.ts
│ ├── createAssociationApply.ts
│ ├── getNumberedList.ts
│ ├── setAssociations.ts
│ ├── addTableColumnConstraint.ts
│ └── migrateEnumValues.ts
├── migrationTypes
│ ├── dropTable.ts
│ ├── removeIndex.ts
│ ├── removeColumns.ts
│ ├── makeColumnAllowNull.ts
│ ├── skip.ts
│ ├── makeColumnNotUnique.ts
│ ├── removeEnumValues.ts
│ ├── removeUniqueConstraintIndex.ts
│ ├── init.ts
│ ├── uuidNonNull.ts
│ ├── custom.ts
│ ├── renameIndex.ts
│ ├── addIndex.ts
│ ├── renameEnum.ts
│ ├── makeColumnUnique.ts
│ ├── index.ts
│ ├── cascadeWithParent.ts
│ ├── addCascadeWithParent.ts
│ └── renameConstraint.ts
├── index.ts
├── checks
│ ├── index.ts
│ ├── matchingBelongsTo.ts
│ ├── allowNullConstraint.ts
│ ├── extraneousTables.ts
│ ├── model.ts
│ ├── columnDefinitions.ts
│ ├── uniqueConstraint.ts
│ ├── belongsToAssociation.ts
│ ├── columnType.ts
│ ├── joinBelongsTo.ts
│ ├── associationConfig.ts
│ └── columnDefinition.ts
├── routes.ts
├── enums.ts
└── Logger.ts
├── .prettierrc
├── .eslintignore
├── generateMixins
├── helpers
│ ├── pascalCase.ts
│ ├── getKeys.ts
│ ├── indexPaths.ts
│ ├── index.ts
│ ├── getCompilerOptions.ts
│ ├── setConfigDefaults.ts
│ ├── determineModelToExtend.ts
│ ├── createBase.ts
│ ├── associationDefinitionToSections.ts
│ ├── calculateMixinAttributes.ts
│ └── serializeAssociationType.ts
├── Handlebars.ts
├── ChangeCase.ts
├── base.hbs
└── index.ts
├── .editorconfig
├── tsconfig.json
├── .vscode
└── settings.json
├── .pre-commit-config.yaml
├── tslint.json
├── .travis.yml
├── fix_absolute_imports.js
├── package.json
└── .eslintrc.js
/generators/migrations/changeEnumColumn/attributes.hbs:
--------------------------------------------------------------------------------
1 | {{{ attributes }}}
--------------------------------------------------------------------------------
/generators/migrationTypes/tableColumnsMigration/definition.hbs:
--------------------------------------------------------------------------------
1 | {{{ definition }}}
2 |
--------------------------------------------------------------------------------
/generators/migrations/addColumns/get-row-defaults.hbs:
--------------------------------------------------------------------------------
1 | getRowDefaults: {{{ getRowDefaults }}},
2 |
--------------------------------------------------------------------------------
/generators/logger.ts:
--------------------------------------------------------------------------------
1 | import Logger from '@wildebeest/Logger';
2 |
3 | export default new Logger();
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | *.hbs
2 | .eslintignore
3 | .editorconfig
4 | .gitignore
5 | yarn.lock
6 | .prettierignore
7 |
--------------------------------------------------------------------------------
/generators/migrationTypes/dbMigration/header.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | * {{#if comment}}{{{ comment }}}{{else}}TODO{{/if}}
3 | */
4 |
5 |
--------------------------------------------------------------------------------
/generators/migrationTypes/tableColumnMigration/template.hbs:
--------------------------------------------------------------------------------
1 | tableName: '{{ modelTableName }}',
2 | columnName: '{{ columnName }}',
--------------------------------------------------------------------------------
/src/models/migration/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A database model for a Migration request.
3 | */
4 |
5 | export { default } from './Migration';
6 |
--------------------------------------------------------------------------------
/generators/migrationTypes/tableColumnsMigration/column.hbs:
--------------------------------------------------------------------------------
1 | // {{{ comment }}}
2 | {{ columnName }}: {
3 | {{> tableColumnsMigrationDefinition }}
4 | },
5 |
--------------------------------------------------------------------------------
/src/models/migrationLock/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A database model for a MigrationLock request.
3 | */
4 |
5 | export { default } from './MigrationLock';
6 |
--------------------------------------------------------------------------------
/src/views/error.hbs:
--------------------------------------------------------------------------------
1 |
Migration Failure :(
2 |
3 | {{ message }}
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/views/success.hbs:
--------------------------------------------------------------------------------
1 | Migration Successful!
2 |
3 |
4 | {{message}}
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/generators/migrationTypes/tableColumnsMigration/columns.hbs:
--------------------------------------------------------------------------------
1 | getColumns: ({ DataTypes }) => ({
2 | {{#each columns}}
3 | {{> tableColumnsMigrationColumn }}
4 | {{/each}}
5 | }),
6 |
--------------------------------------------------------------------------------
/generators/migrationTypes/tableColumnsMigration/constraint.hbs:
--------------------------------------------------------------------------------
1 | {{#if tableName }}{{ curly true }} columnName: '{{ columnName }}', tableName: '{{ tableName }}' {{ curly false }}{{else}}'{{ columnName }}'{{/if}}
--------------------------------------------------------------------------------
/generators/migrationTypes/tablesMigration/tables.hbs:
--------------------------------------------------------------------------------
1 | tableName: {{#if (listLen tables)}}{{#listComma tables }}'{{ modelTableName }}'{{/listComma}}{{else}}{{#each tables}}'{{ modelTableName }}'{{/each}}{{/if}},
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "all",
8 | "arrowParens": "always"
9 | }
10 |
--------------------------------------------------------------------------------
/generators/migrations/dropTable/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { dropTable } from '@wildebeest';
4 |
5 | module.exports = dropTable({
6 | {{> tableColumnsMigration }}
7 | });
8 |
--------------------------------------------------------------------------------
/generators/migrationTypes/changeEnumAttributes/template.hbs:
--------------------------------------------------------------------------------
1 | name: '{{ enumName }}',
2 | attributes: [{{#listComma attributes }}'{{ name }}'{{/listComma}}],
3 | tableName: '{{ tableName }}',
4 | columnName: '{{ columnName }}',
--------------------------------------------------------------------------------
/generators/migrations/addColumns/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { addColumns } from '@wildebeest';
4 |
5 | module.exports = addColumns({
6 | {{> tableColumnsMigration }}
7 | });
8 |
--------------------------------------------------------------------------------
/generators/migrations/createTable/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { createTable } from '@wildebeest';
4 |
5 | module.exports = createTable({
6 | {{> tableColumnsMigration }}
7 | });
8 |
--------------------------------------------------------------------------------
/generators/migrations/uuidNonNull/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { uuidNonNull } from '@wildebeest';
4 |
5 | module.exports = uuidNonNull({
6 | {{> tableColumnMigration }}
7 | });
8 |
--------------------------------------------------------------------------------
/generators/migrations/removeColumns/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { removeColumns } from '@wildebeest';
4 |
5 | module.exports = removeColumns({
6 | {{> tableColumnsMigration }}
7 | });
8 |
--------------------------------------------------------------------------------
/generators/migrations/addEnumValues/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { addEnumValues } from '@wildebeest';
4 |
5 | module.exports = addEnumValues({
6 | {{> changeEnumAttributes}}
7 |
8 | });
9 |
--------------------------------------------------------------------------------
/generators/migrations/makeColumnNonNull/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { makeColumnNonNull } from '@wildebeest';
4 |
5 | module.exports = makeColumnNonNull({
6 | {{> tableColumnMigration}}
7 |
8 | });
9 |
--------------------------------------------------------------------------------
/generators/migrations/makeColumnUnique/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { makeColumnUnique } from '@wildebeest';
4 |
5 | module.exports = makeColumnUnique({
6 | {{> tableColumnMigration }}
7 |
8 | });
9 |
--------------------------------------------------------------------------------
/generators/migrations/removeEnumValues/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { removeEnumValues } from '@wildebeest';
4 |
5 | module.exports = removeEnumValues({
6 | {{> changeEnumAttributes }}
7 |
8 | });
9 |
--------------------------------------------------------------------------------
/generators/migrations/cascadeWithParent/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { cascadeWithParent } from '@wildebeest';
4 |
5 | module.exports = cascadeWithParent({
6 | {{> tableColumnMigration }}
7 |
8 | });
9 |
--------------------------------------------------------------------------------
/generators/migrations/removeNonNullColumn/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { removeNonNullColumn } from '@wildebeest';
4 |
5 | module.exports = removeNonNullColumn({
6 | {{> tableColumnMigration }}
7 | });
8 |
--------------------------------------------------------------------------------
/generators/migrations/renameEnum/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { renameEnum } from '@wildebeest';
4 |
5 | module.exports = renameEnum({
6 | oldName: '{{ oldName }}',
7 | newName: '{{ newName }}',
8 | });
9 |
--------------------------------------------------------------------------------
/generators/migrations/addCascadeWithParent/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { addCascadeWithParent } from '@wildebeest';
4 |
5 | module.exports = addCascadeWithParent({
6 | {{> tableColumnMigration }}
7 |
8 | });
9 |
--------------------------------------------------------------------------------
/generators/migrations/makeColumnAllowNull/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { makeColumnAllowNull } from '@wildebeest';
4 |
5 | module.exports = makeColumnAllowNull({
6 | {{> tableColumnMigration }}
7 |
8 | });
9 |
--------------------------------------------------------------------------------
/generators/migrations/renameIndex/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { renameIndex } from '@wildebeest';
4 |
5 | module.exports = renameIndex({
6 | oldName: '{{ oldName }}',
7 | newName: '{{ newName }}',
8 | });
9 |
--------------------------------------------------------------------------------
/src/models/index.ts:
--------------------------------------------------------------------------------
1 | // local
2 | import Migration from './migration';
3 | import MigrationLock from './migrationLock';
4 |
5 | /**
6 | * Database models needed to run migrations
7 | */
8 | export default {
9 | Migration,
10 | MigrationLock,
11 | };
12 |
--------------------------------------------------------------------------------
/src/classes/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Class definitions for wildebeest
3 | */
4 |
5 | // local
6 | export { default as Wildebeest } from './Wildebeest';
7 | export { default as WildebeestDb } from './WildebeestDb';
8 | export { default as WildebeestModel } from './WildebeestModel';
9 |
--------------------------------------------------------------------------------
/generators/migrations/renameTable/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { renameTable } from '@wildebeest';
4 |
5 | module.exports = renameTable({
6 | oldName: '{{ oldName }}',
7 | newName: '{{ newName }}',
8 | renameConstraints: true,
9 | });
10 |
--------------------------------------------------------------------------------
/src/utils/mkEnum.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Make an enum compatible with types
3 | *
4 | * @param x - The enum
5 | * @returns The enum type enforced
6 | */
7 | export default function mkEnum<
8 | T extends { [index: string]: U },
9 | U extends string
10 | >(x: T): T {
11 | return x;
12 | }
13 |
--------------------------------------------------------------------------------
/generators/migrations/renameColumn/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { renameColumn } from '@wildebeest';
4 |
5 | module.exports = renameColumn({
6 | tableName: '{{ modelTableName }}',
7 | oldName: '{{ oldName }}',
8 | newName: '{{ newName }}',
9 | });
10 |
--------------------------------------------------------------------------------
/generators/migrationTypes/dbMigration/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | export default {
3 | up: (migrator, { queryInterface, DataTypes }) => {
4 | throw new Error('TODO');
5 | },
6 | down: (migrator, { queryInterface, DataTypes }) => {
7 | throw new Error('TODO');
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/generators/migrations/renameConstraint/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { renameConstraint } from '@wildebeest';
4 |
5 | module.exports = renameConstraint({
6 | tableName: '{{ modelTableName }}',
7 | oldName: '{{ oldName }}',
8 | newName: '{{ newName }}',
9 | });
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | .gitignore
2 | .git/*
3 | .github/*
4 | **/docs/*
5 | **/node_modules/*
6 | **/build/*
7 | **/dist/*
8 | **/builds/*
9 | **/coverage/*
10 | **/deprecated/*
11 | **/.storybook-dist/*
12 | **/cypress/*
13 | **/docs/*
14 | **/Internal-Documentation/*
15 | *.testjs
16 | *.testts
17 | *__snapshots__/*
18 |
--------------------------------------------------------------------------------
/generators/migrations/changeOnDelete/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { changeOnDelete } from '@wildebeest';
4 |
5 | module.exports = changeOnDelete({
6 | {{> tableColumnMigration }}
7 |
8 | oldOnDelete: '{{ oldOnDelete }}',
9 | newOnDelete: '{{ newOnDelete }}',
10 | });
11 |
--------------------------------------------------------------------------------
/generators/migrations/addUniqueConstraintIndex/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { addUniqueConstraintIndex } from '@wildebeest';
4 |
5 | module.exports = addUniqueConstraintIndex({
6 | tableName: '{{ modelTableName }}',
7 | fields: [{{#listComma columns}}'{{ columnName }}'{{/listComma}}],
8 | });
9 |
--------------------------------------------------------------------------------
/generators/migrations/removeUniqueConstraintIndex/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { removeUniqueConstraintIndex } from '@wildebeest';
4 |
5 | module.exports = removeUniqueConstraintIndex({
6 | tableName: '{{ modelTableName }}',
7 | fields: [{{#listComma columns}}'{{ columnName }}'{{/listComma}}],
8 | });
9 |
--------------------------------------------------------------------------------
/src/utils/sleepPromise.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Sleep in a promise
3 | *
4 | * @param sleepTime - The time to sleep
5 | * @returns Resolves promise
6 | */
7 | export default function sleepPromise(sleepTime: number): Promise {
8 | return new Promise((resolve) =>
9 | setTimeout(() => resolve(sleepTime), sleepTime),
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/generators/migrationTypes/tableColumnsMigration/template.hbs:
--------------------------------------------------------------------------------
1 | tableName: '{{ modelTableName }}',
2 | {{> tableColumnsMigrationColumns }}
3 | {{#if getRowDefaults}}
4 | {{> addColumnsGetRowDefaults }}
5 | {{/if}}
6 | {{#if (listLen constraints)}}
7 | constraints: [{{#listComma constraints}}{{> tableColumnsMigrationConstraint }}{{/listComma}}],
8 | {{/if}}
--------------------------------------------------------------------------------
/generators/migrations/createTable/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Create a new db table
3 | */
4 | export default {
5 | configure: ({ modelTableName, model }) => ({
6 | name: `create-table-${modelTableName}`,
7 | comment: `Create the ${model} table.`,
8 | }),
9 | description: 'Create a new db table',
10 | type: 'tableColumnsMigration',
11 | };
12 |
--------------------------------------------------------------------------------
/generators/migrations/dropTable/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Drop one of the tables
3 | */
4 | export default {
5 | configure: ({ modelTableName, model }) => ({
6 | name: `drop-table-${modelTableName}`,
7 | comment: `Drop the ${model} table.`,
8 | }),
9 | description: 'Drop one of the tables',
10 | suggestOnly: true,
11 | type: 'tableColumnsMigration',
12 | };
13 |
--------------------------------------------------------------------------------
/generators/migrations/rename-enum-value/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { renameEnumValue } from '@wildebeest';
4 |
5 | module.exports = renameEnumValue({
6 | name: '{{ enumName }}',
7 | oldValue: '{{ oldValue }}',
8 | newValue: '{{ newValue }}',
9 | tableName: '{{ tableName }}',
10 | columnName: '{{ columnName }}',
11 | });
12 |
--------------------------------------------------------------------------------
/generators/migrations/addIndex/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { addIndex } from '@wildebeest';
4 |
5 | module.exports = addIndex({
6 | tableName: '{{ modelTableName }}',
7 | fields: [{{#listComma columns}}'{{ columnName }}'{{/listComma}}],
8 | {{#ifNotEquals method "BTREE"}}
9 | method: '{{ method }}',
10 | {{/ifNotEquals}}
11 | });
12 |
--------------------------------------------------------------------------------
/src/utils/pascalCase.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import camelCase from 'lodash/camelCase';
3 | import upperFirst from 'lodash/upperFirst';
4 |
5 | /**
6 | * Make a word pascal case
7 | *
8 | * @param word - The word to modify
9 | * @returns The word in pascal case
10 | */
11 | export default function pascalCase(word: string): string {
12 | return upperFirst(camelCase(word));
13 | }
14 |
--------------------------------------------------------------------------------
/generators/migrations/removeIndex/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { removeIndex } from '@wildebeest';
4 |
5 | module.exports = removeIndex({
6 | tableName: '{{ modelTableName }}',
7 | fields: [{{#listComma columns}}'{{ columnName }}'{{/listComma}}],
8 | {{#ifNotEquals method "BTREE"}}
9 | method: '{{ method }}',
10 | {{/ifNotEquals}}
11 | });
12 |
--------------------------------------------------------------------------------
/src/utils/getBackoffTime.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Exponential back-off algorithm https://gist.github.com/kitcambridge/11101250
3 | *
4 | * @param localAttempts - Number of attemps so far
5 | * @param delay - The delay interval
6 | * @returns The time to wait
7 | */
8 | export default (localAttempts: number, delay: number): number =>
9 | Math.floor(Math.random() * 2 ** localAttempts * delay);
10 |
--------------------------------------------------------------------------------
/generateMixins/helpers/pascalCase.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import camelCase from 'lodash/camelCase';
3 | import upperFirst from 'lodash/upperFirst';
4 |
5 | /**
6 | * Make a word pascal case
7 | *
8 | * @param word - The word to modify
9 | * @returns The word in pascal case
10 | */
11 | export default function pascalCase(word: string): string {
12 | return upperFirst(camelCase(word));
13 | }
14 |
--------------------------------------------------------------------------------
/generators/migrationTypes/dbMigration/actions.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Add a new migration file
3 | *
4 | * @param data - The prompt answers
5 | * @param repo - The repository configuration
6 | * @returns The action
7 | */
8 | export const ADD_MIGRATION_FILE = ({ templateName }) => ({
9 | path: '{{ location }}/{{ migrationNumber }}-{{ name }}',
10 | templateName,
11 | type: 'addFile',
12 | });
13 |
--------------------------------------------------------------------------------
/src/migrationTypes/dropTable.ts:
--------------------------------------------------------------------------------
1 | // migrators
2 | import reverseMigrator from '@wildebeest/utils/reverseMigrator';
3 |
4 | // local
5 | import createTable from './createTable';
6 |
7 | /**
8 | * Remove a table from the db and all associated parts
9 | *
10 | * @param options - Options for dropping a table
11 | * @returns The drop table migrator
12 | */
13 | export default reverseMigrator(createTable);
14 |
--------------------------------------------------------------------------------
/src/migrationTypes/removeIndex.ts:
--------------------------------------------------------------------------------
1 | // migrators
2 | import reverseMigrator from '@wildebeest/utils/reverseMigrator';
3 |
4 | // local
5 | import addIndex from './addIndex';
6 |
7 | /**
8 | * Remove an existing index
9 | *
10 | * @param options - Options for making a unique constraint across multiple columns
11 | * @returns The remove index migrator
12 | */
13 | export default reverseMigrator(addIndex);
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | insert_final_newline = true
9 | indent_style = space
10 | indent_size = 2
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
15 |
16 | [*.hbs]
17 | trim_trailing_whitespace = false
18 | insert_final_newline = false
19 | indent_style = space
20 | indent_size = 2
21 |
--------------------------------------------------------------------------------
/generators/migrations/uuidNonNull/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Make a uuid column non null
3 | */
4 |
5 | export default {
6 | configure: ({ modelTableName, columnName, model }) => ({
7 | name: `uuid-non-null-${modelTableName}-${columnName}`,
8 | comment: `Change column ${model}.${columnName} to be NON NULL.`,
9 | }),
10 | description: 'Make a uuid column non null',
11 | type: 'tableAssociationColumn',
12 | };
13 |
--------------------------------------------------------------------------------
/generators/migrations/renameS3Files/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { renameS3Files } from '@wildebeest';
4 |
5 | module.exports = renameS3Files({
6 | tableName: '{{ modelTableName }}',
7 | Bucket: {{{ bucket }}},
8 | attributes: '{{{ attributes }}}',
9 | getOldKey: {{{ getOldKey }}},
10 | getNewKey: {{{ getNewKey }}},
11 | {{#if remove}}
12 | remove: true,
13 | {{/if}}
14 | });
15 |
--------------------------------------------------------------------------------
/src/migrationTypes/removeColumns.ts:
--------------------------------------------------------------------------------
1 | // migrators
2 | import reverseMigrator from '@wildebeest/utils/reverseMigrator';
3 |
4 | // local
5 | import addColumns from './addColumns';
6 |
7 | /**
8 | * Remove multiple columns on up, and add them back on down
9 | *
10 | * @param options - Options for adding new columns to a table
11 | * @returns The remove columns migrator
12 | */
13 | export default reverseMigrator(addColumns);
14 |
--------------------------------------------------------------------------------
/generators/generators/migration-type-generator/template.hbs:
--------------------------------------------------------------------------------
1 | {{> generatorImports }}
2 |
3 | /**
4 | * A higher order generator that creates a {{ sentenceCase container }} migrator.
5 | *
6 | * {{{ comment }}}
7 | */
8 | export default {
9 | {{#if wantActions}}
10 | actions,
11 | {{/if}}
12 | {{#if generatorType}}
13 | parentType: '{{ generatorType }}',
14 | {{/if}}
15 | {{#if wantPrompts}}
16 | prompts,
17 | {{/if}}
18 | };
19 |
--------------------------------------------------------------------------------
/generators/generators/migration/template.hbs:
--------------------------------------------------------------------------------
1 | {{> generatorImports }}
2 |
3 | /**
4 | * {{{ comment }}}
5 | */
6 | export default {
7 | configure: () => ({
8 | name: `{{ paramCase container }}-${{ curly true }}{{{ nameExt }}}{{ curly false }}`,
9 | }),
10 | description: '{{{ comment }}}',
11 | {{#if wantPrompts}}
12 | prompts,
13 | {{/if}}
14 | {{#if generatorType}}
15 | type: '{{ generatorType }}',
16 | {{/if}}
17 | };
18 |
--------------------------------------------------------------------------------
/generators/migrationTypes/tableMigration/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Create a migration that modifies a single table
3 | */
4 | export default {
5 | parentType: 'dbMigration',
6 | prompts: {
7 | modelPath: (repo, { suggestOnly }) => ({
8 | message: `What db model are you migrating?${
9 | suggestOnly ? ' (suggestOnly)' : ''
10 | }`,
11 | suggestOnly,
12 | type: 'modelPath',
13 | }),
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/src/migrationTypes/makeColumnAllowNull.ts:
--------------------------------------------------------------------------------
1 | // migrators
2 | import reverseMigrator from '@wildebeest/utils/reverseMigrator';
3 |
4 | // local
5 | import makeColumnNonNull from './makeColumnNonNull';
6 |
7 | /**
8 | * Make a column allow null values
9 | *
10 | * @param options - The make column allow null options
11 | * @returns The make column allow null migrator
12 | */
13 | export default reverseMigrator(makeColumnNonNull);
14 |
--------------------------------------------------------------------------------
/generators/migrations/makeColumnNonNull/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Make a column non null
3 | */
4 | export default {
5 | configure: ({ modelTableName, columnName, model }) => ({
6 | name: `make-column-non-null-${modelTableName}-${columnName}`,
7 | comment: `Make the column ${model}.${columnName} non null.`,
8 | }),
9 | description: 'Make a column non null',
10 | // columnSuggestOnly: false,
11 | type: 'tableColumnMigration',
12 | };
13 |
--------------------------------------------------------------------------------
/src/migrationTypes/skip.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { MigrationDefinition, ModelMap } from '@wildebeest/types';
3 |
4 | /**
5 | * Skip the migrator because it was later removed
6 | *
7 | * @returns The skip migrator
8 | */
9 | export default function skip(): MigrationDefinition<
10 | TModels
11 | > {
12 | return {
13 | up: () => Promise.resolve(null),
14 | down: () => Promise.resolve(null),
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/generators/migrationTypes/tableColumnMigration/typeDefs.ts:
--------------------------------------------------------------------------------
1 | // ///// //
2 | // Types //
3 | // ///// //
4 |
5 | /**
6 | * Information about a table attribute
7 | *
8 | * @typedef {Object} AttributeDefinition
9 | * @property {string} type - Type (attribute or association)
10 | * @property {string} relativePath - The relative path to the definition of the column
11 | * @property {string} columnName - The name of the associated column
12 | */
13 |
--------------------------------------------------------------------------------
/generators/migrations/bulkInsert/insert.hbs:
--------------------------------------------------------------------------------
1 | // TODO
2 | const [TODO] = await queryInterface.sequelize.query(`SELECT * FROM ${TODO}`);
3 |
4 | // Create the {{ model }} inputs
5 | const inputs = TODO.map(() => ({
6 | id: uuidv4(),
7 | updatedAt: new Date(),
8 | createdAt: new Date(),
9 | TODO,
10 | }));
11 |
12 | // Bulk insert {{ pluralCase model }}
13 | if (inputs.length > 0) await queryInterface.bulkInsert('{{ pluralCase model }}', inputs);
--------------------------------------------------------------------------------
/src/utils/createAttributes.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { Attributes } from '@wildebeest/types';
3 |
4 | /**
5 | * Identity function that will type-enforce attribute definitions
6 | *
7 | * @param attributes - The db model attributes
8 | * @returns The type-enforced attributes
9 | */
10 | export default function createAttributes(
11 | attributes: TAttributes,
12 | ): TAttributes {
13 | return attributes;
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/escapeJson.ts:
--------------------------------------------------------------------------------
1 | import type { ObjByString } from '@wildebeest/types';
2 |
3 | /**
4 | * Escape JSON that may have single quotes in it before inserting into SQL statement
5 | *
6 | * @param obj - The object to check
7 | */
8 | export default function escapeJson(
9 | obj: T[] | T,
10 | ): string {
11 | return `cast('${JSON.stringify(obj)
12 | .split("'")
13 | .join("' || CHR(39) || '")}' as json)`;
14 | }
15 |
--------------------------------------------------------------------------------
/generators/migrations/makeColumnUnique/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Make a table column unique
3 | */
4 | export default {
5 | configure: ({ modelTableName, columnName, model }) => ({
6 | name: `make-column-unique-${modelTableName}-${columnName}`,
7 | comment: `Add a constraint to make ${model}.${columnName} unique.`,
8 | }),
9 | description: 'Make a table column unique',
10 | columnSuggestOnly: false,
11 | type: 'tableColumnMigration',
12 | };
13 |
--------------------------------------------------------------------------------
/src/migrationTypes/makeColumnNotUnique.ts:
--------------------------------------------------------------------------------
1 | // migrators
2 | import reverseMigrator from '@wildebeest/utils/reverseMigrator';
3 |
4 | // local
5 | import makeColumnUnique from './makeColumnUnique';
6 |
7 | /**
8 | * Remove a unique constrain on a column
9 | *
10 | * @param options - Options for making a column unique
11 | * @returns The remove unique constraint index migrator migrator
12 | */
13 | export default reverseMigrator(makeColumnUnique);
14 |
--------------------------------------------------------------------------------
/src/migrationTypes/removeEnumValues.ts:
--------------------------------------------------------------------------------
1 | // migrators
2 | import reverseMigrator from '@wildebeest/utils/reverseMigrator';
3 |
4 | // local
5 | import addEnumValues from './addEnumValues';
6 |
7 | /**
8 | * Remove values from an enum on up, and add them back on down
9 | *
10 | * @param options - Options for removing attributes from the enum
11 | * @returns The remove enum values migrator
12 | */
13 | export default reverseMigrator(addEnumValues);
14 |
--------------------------------------------------------------------------------
/generators/migrations/bulkDelete/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // external
3 | import uuidv4 from 'uuid/v4';
4 |
5 | // wildebeest
6 | import { custom } from '@wildebeest';
7 |
8 | throw new Error('FIX TODO IN MIGRATION {{ migrationNumber }}');
9 | module.exports = custom({
10 | up: async ({ queryT }) =>
11 | queryT.insert('{{ modelTableName }}', []), // TODO
12 | down: async ({ queryT }) => queryT.delete('{{ modelTableName }}', { TODO }),
13 | });
14 |
--------------------------------------------------------------------------------
/generators/migrations/bulkInsert/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // external
3 | import uuidv4 from 'uuid/v4';
4 |
5 | // wildebeest
6 | import { custom } from '@wildebeest';
7 |
8 | throw new Error('FIX TODO IN MIGRATION {{ migrationNumber }}');
9 | module.exports = custom({
10 | up: async ({ queryT }) =>
11 | queryT.insert('{{ modelTableName }}', []), // TODO
12 | down: async ({ queryT }) => queryT.delete('{{ modelTableName }}', { TODO }),
13 | });
14 |
--------------------------------------------------------------------------------
/generators/migrations/changeColumn/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { changeColumn } from '@wildebeest';
4 |
5 | module.exports = changeColumn({
6 | {{> tableColumnMigration }}
7 |
8 | getOldColumn: ({ DataTypes }) => ({
9 | {{> tableColumnsMigrationDefinition definition=oldDefinition }}
10 | }),
11 | getNewColumn: ({ DataTypes }) => ({
12 | {{> tableColumnsMigrationDefinition definition=newDefinition }}
13 | }),
14 | });
15 |
--------------------------------------------------------------------------------
/generators/migrations/makeColumnAllowNull/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Make a column allow null values
3 | */
4 | export default {
5 | configure: ({ modelTableName, columnName, model }) => ({
6 | name: `make-column-allow-null-${modelTableName}-${columnName}`,
7 | comment: `Make the column ${model}.${columnName} allow null values.`,
8 | }),
9 | description: 'Make a column allow null values',
10 | // columnSuggestOnly: false,
11 | type: 'tableColumnMigration',
12 | };
13 |
--------------------------------------------------------------------------------
/src/utils/isEnum.ts:
--------------------------------------------------------------------------------
1 | // db
2 | import * as sequelize from 'sequelize';
3 |
4 | /**
5 | * Check if an attribute is an enum
6 | *
7 | * @param column - The column definition to check
8 | * @returns True if the column is an enum definition
9 | */
10 | export default function isEnum(
11 | column: sequelize.ModelAttributeColumnOptions,
12 | ): boolean {
13 | return typeof column.type === 'string'
14 | ? column.type === 'ENUM'
15 | : column.type.key === 'ENUM';
16 | }
17 |
--------------------------------------------------------------------------------
/generators/generators/migration-type/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Create a higher order migration definition
3 | *
4 | * TODO wildebeest
5 | */
6 | export default {
7 | defaultName: 'createTable',
8 | description: 'Create a higher order migration definition',
9 | location: 'migrations/migrationTypes',
10 | mode: 'backend',
11 | nameTransform: 'camelCase',
12 | objectParams: 'options',
13 | skipActions: true,
14 | skipReturnPrompts: true,
15 | type: 'functionFile',
16 | };
17 |
--------------------------------------------------------------------------------
/generators/migrations/addCascadeWithParent/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Migrate a column so that it cascades with its parent
3 | */
4 | export default {
5 | configure: ({ modelTableName, columnName, model }) => ({
6 | name: `add-cascade-with-parent-${modelTableName}-${columnName}`,
7 | comment: `Add cascade with parent constraint to ${model}.${columnName}.`,
8 | }),
9 | description: 'Migrate a column so that it cascades with its parent',
10 | type: 'tableAssociationColumn',
11 | };
12 |
--------------------------------------------------------------------------------
/src/utils/createHooks.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { WildebeestModel } from '@wildebeest/classes';
3 | import { HookOptions, ModelMap } from '@wildebeest/types';
4 |
5 | /**
6 | * Create database model hooks for a db model
7 | *
8 | * @param hooks - The hooks
9 | * @returns The type-enforced hooks
10 | */
11 | export default function createHooks>(
12 | hooks: Partial>,
13 | ): Partial> {
14 | return hooks;
15 | }
16 |
--------------------------------------------------------------------------------
/generators/migrations/removeEnumValues/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Remove values from an existing enum
3 | */
4 | export default {
5 | configure: ({ enumName, attributes }) => ({
6 | name: `remove-enum-values-from-${enumName}`,
7 | comment: `Remove the attributes: ${attributes
8 | .map((attr) => attr.name)
9 | .join(', ')} from ${enumName}.`,
10 | }),
11 | description: 'Remove values from an existing enum',
12 | attributesSuggestOnly: true,
13 | type: 'changeEnumAttributes',
14 | };
15 |
--------------------------------------------------------------------------------
/generators/migrationTypes/tablesMigration/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A higher order generator that creates a Tables migration migrator.
3 | *
4 | * Migrate multiple tables at the same time
5 | */
6 | export default {
7 | parentType: 'dbMigration',
8 | prompts: {
9 | tables: {
10 | childName: 'table',
11 | message: 'Choose another table?',
12 | prompts: {
13 | modelPath: {
14 | type: 'modelPath',
15 | },
16 | },
17 | type: 'recursive',
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/generators/migrations/bulkDelete/delete.hbs:
--------------------------------------------------------------------------------
1 | // Get the {{ pluralCase model }}
2 | const [{{ pluralCase model }}] = await queryInterface.sequelize.query('SELECT * FROM "{{ pluralCase model }}"');
3 |
4 | // Update TODO
5 | await Promise.all({{ pluralCase model }}.map(() => queryInterface.sequelize.query(`
6 | UPDATE "${TODO}"
7 | SET "${TODO}"='${TODO}'
8 | WHERE "${TODO}"='${TODO}';
9 | `)));
10 |
11 | // Bulk delete {{ pluralCase model }}
12 | await queryInterface.bulkDelete('{{ pluralCase model }}', {
13 | TODO
14 | });
--------------------------------------------------------------------------------
/generators/generators/migration-type-generator/index.ts:
--------------------------------------------------------------------------------
1 | // generators
2 | import migration from '../migration/prompts';
3 |
4 | /**
5 | * Create a new generator to accompany a migration type
6 | *
7 | * TODO wildebeest
8 | */
9 | export default {
10 | description: 'Create a new generator to accompany a migration type',
11 | displayGeneratorName: 'migration-type',
12 | location: 'migrationTypes',
13 | prompts: {
14 | generatorType: migration.prompts.generatorType,
15 | },
16 | type: 'generatorContainer',
17 | };
18 |
--------------------------------------------------------------------------------
/generators/migrations/addEnumValues/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Add values to an existing enum
3 | */
4 | export default {
5 | configure: ({ attributes, enumName }) => ({
6 | name: `add-enum-values-to-${enumName}`,
7 | comment: `Add the attributes: ${attributes
8 | .map((attr) => attr.name)
9 | .join(', ')} to ${enumName}`,
10 | }),
11 | description: 'Add values to an existing enum',
12 | attributesSuggestOnly: true, // TODO remove when enums can recursively detect
13 | type: 'changeEnumAttributes',
14 | };
15 |
--------------------------------------------------------------------------------
/generators/migrations/cascadeWithParent/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Update a column to have an on delete cascade when it's parent is cascaded
3 | */
4 | export default {
5 | configure: ({ modelTableName, columnName, model }) => ({
6 | name: `cascade-with-parent-${modelTableName}-${columnName}`,
7 | comment: `Update constraint for the column ${model}.${columnName} to cascade with parent.`,
8 | }),
9 | description:
10 | "Update a column to have an on delete cascade when it's parent is cascaded",
11 | type: 'tableAssociationColumn',
12 | };
13 |
--------------------------------------------------------------------------------
/src/migrationTypes/removeUniqueConstraintIndex.ts:
--------------------------------------------------------------------------------
1 | // migrators
2 | import reverseMigrator from '@wildebeest/utils/reverseMigrator';
3 |
4 | // local
5 | import addUniqueConstraintIndex from './addUniqueConstraintIndex';
6 |
7 | /**
8 | * Remove a unique constrain across multiple columns on up, and add it back on down
9 | *
10 | * @param options - Options for making a unique constraint across multiple columns
11 | * @returns The remove unique constraint index migrator migrator
12 | */
13 | export default reverseMigrator(addUniqueConstraintIndex);
14 |
--------------------------------------------------------------------------------
/generators/migrations/custom/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A custom migration
3 | */
4 | export default {
5 | configure: ({ modelTableName, name }) => ({
6 | name: `${modelTableName}-${name}`,
7 | }),
8 | description: 'A custom migration',
9 | prompts: {
10 | name: {
11 | message: 'What is the migration name?',
12 | nameTransform: 'paramCase',
13 | type: 'name',
14 | },
15 | comment: {
16 | message: 'What is the migration comment?',
17 | type: 'comment',
18 | },
19 | },
20 | type: 'tableMigration',
21 | };
22 |
--------------------------------------------------------------------------------
/generators/migrations/removeNonNullColumn/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Remove a column that is not allowed to be null, and when down is run, allow the column to be null
3 | */
4 | export default {
5 | configure: ({ modelTableName, columnName, model }) => ({
6 | name: `remove-non-null-column-${modelTableName}-${columnName}`,
7 | comment: `Remove non null column ${model}.${columnName}.`,
8 | }),
9 | description:
10 | 'Remove a column that is not allowed to be null, and when down is run, allow the column to be null',
11 | type: 'tableColumnMigration',
12 | };
13 |
--------------------------------------------------------------------------------
/generators/migrations/addColumns/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Add columns to a table
3 | */
4 | export default {
5 | configure: ({ modelTableName, columns, model }) => ({
6 | name: `add-columns-${modelTableName}-${columns
7 | .map(({ columnName }) => columnName)
8 | .join('-')}`,
9 | comment: `Add columns (${columns
10 | .map(({ columnName }) => columnName)
11 | .join(', ')}) to ${model}.`, // eslint-disable-line max-len
12 | }),
13 | description: 'Add columns to a table',
14 | withGetRowDefaults: true,
15 | type: 'tableColumnsMigration',
16 | };
17 |
--------------------------------------------------------------------------------
/src/utils/clearSchema.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import sequelize from 'sequelize';
3 |
4 | /**
5 | * Re create a schema
6 | *
7 | * @param queryInterface - The sequelize queryInterface
8 | * @param schema - The name of the schema
9 | * @returns The refresh schema promise
10 | */
11 | export default async function clearSchema(
12 | queryInterface: sequelize.QueryInterface,
13 | schema = 'public',
14 | ): Promise {
15 | // Drop the schema
16 | await queryInterface.dropSchema(schema);
17 |
18 | // Re-create it
19 | await queryInterface.createSchema(schema);
20 | }
21 |
--------------------------------------------------------------------------------
/generators/migrations/renameTable/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Rename a table
3 | */
4 | export default {
5 | configure: ({ newName, oldName }) => ({
6 | name: `rename-table-to-${newName}`,
7 | comment: `Rename the table ${oldName} to ${newName}.`,
8 | }),
9 | description: 'Rename a table',
10 | prompts: {
11 | oldName: {
12 | message: 'What was the old name of the table?',
13 | type: 'name',
14 | },
15 | newName: {
16 | message: 'What is the new name of the table?',
17 | type: 'name',
18 | },
19 | },
20 | type: 'dbMigration',
21 | };
22 |
--------------------------------------------------------------------------------
/generators/migrations/changeEnumColumn/template.hbs:
--------------------------------------------------------------------------------
1 | {{> dbMigrationHeader }}
2 | // migrationTypes
3 | import { changeEnumColumn } from '@wildebeest';
4 |
5 | module.exports = changeEnumColumn({
6 | {{> tableColumnMigration }}
7 |
8 | {{#if oldDefault}}
9 | oldDefault: '{{ oldDefault }}',
10 | {{/if}}
11 | {{#if newDefault}}
12 | newDefault: '{{ newDefault }}',
13 | {{/if}}
14 | oldEnum: {
15 | {{> changeEnumColumnAttributes attributes=oldAttributes }}
16 |
17 | },
18 | newEnum: {
19 | {{> changeEnumColumnAttributes attributes=newAttributes }}
20 |
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/src/utils/freshIndexes.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { ModelOptions } from 'sequelize';
3 |
4 | /**
5 | * Copy over indexes to new object. This is to ensure that any inherited options are not just a reference to another tables options.
6 | *
7 | * @param options - The db model options to copy
8 | * @returns The copied options
9 | */
10 | export default function freshIndexes(options?: ModelOptions): ModelOptions {
11 | if (!options || !options.indexes) {
12 | return {};
13 | }
14 | return {
15 | ...options,
16 | indexes: options.indexes.map((index) => ({ ...index })),
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/generators/generators/migration-type/template.hbs:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * {{{ comment }}}
4 | *
5 | {{#if params}}
6 | {{> functionParamsComment }}
7 |
8 | {{/if}}
9 | * @returns The {{ lowerCase (sentenceCase name) }} migrator
10 | */
11 | {{{ defaultExportType }}} function custom(
12 | {{> functionParams }},
13 | ): MigrationDefinition {
14 | const { up, down } = options;
15 | return {
16 | up: async (db, withTransaction) =>
17 | withTransaction((transactionOptions) => TODO),
18 | down: async (db, withTransaction) =>
19 | withTransaction((transactionOptions) => TODO),
20 | };
21 | }
22 |
--------------------------------------------------------------------------------
/generators/migrations/removeIndex/index.ts:
--------------------------------------------------------------------------------
1 | import { method } from '../addIndex/prompts';
2 |
3 | /**
4 | * Remove an existing index
5 | */
6 | export default {
7 | configure: ({ columns, model }) => ({
8 | name: `remove-index-${columns
9 | .map(({ columnName }) => columnName)
10 | .join('-')}`,
11 | comment: `Remove index on ${model} fields: ${columns
12 | .map(({ columnName }) => columnName)
13 | .join(', ')}.`, // eslint-disable-line max-len
14 | }),
15 | description: 'Remove an existing index',
16 | prompts: { method },
17 | type: 'tableColumnsMigration',
18 | };
19 |
--------------------------------------------------------------------------------
/generators/migrations/bulkInsert/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Run a bulk insert
3 | */
4 | export default {
5 | configure: ({ modelTableName, modelContainer, nameExt }) => ({
6 | name: `bulk-insert-${modelTableName}${nameExt ? `-${nameExt}` : ''}`,
7 | comment: `Bulk insert into table [${modelTableName}]{@link module:${modelContainer}}`,
8 | }),
9 | description: 'Run a bulk insert',
10 | prompts: {
11 | nameExt: {
12 | message:
13 | 'Name the migration (will be appended after "bulk-insert-{{ modelTableName }}-"',
14 | type: 'name',
15 | },
16 | },
17 | type: 'tableMigration',
18 | };
19 |
--------------------------------------------------------------------------------
/generators/migrations/bulkDelete/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Bulk delete table rows
3 | */
4 | export default {
5 | configure: ({ modelTableName, modelContainer, nameExt }) => ({
6 | name: `bulk-delete-${modelTableName}${nameExt ? `-${nameExt}` : ''}`,
7 | comment: `Bulk delete from table [${modelTableName}]{@link module:${modelContainer}}`,
8 | }),
9 | description: 'Bulk delete table rows',
10 | prompts: {
11 | nameExt: {
12 | message:
13 | 'Name the migration (will be appended after "bulk-delete-{{ modelTableName }}-"',
14 | type: 'name',
15 | },
16 | },
17 | type: 'tableMigration',
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/getNextNumber.ts:
--------------------------------------------------------------------------------
1 | // local
2 | import { parseNumber } from './getNumberedList';
3 |
4 | /**
5 | * Get the next number from a list of files or folders
6 | *
7 | * Default is to check for folders
8 | *
9 | * ```typescript
10 | * // Returns 0003
11 | * getNextNumber(['0001-my-first.ts', '0002-my-second.ts']);
12 | * ```
13 | *
14 | * @param items - The list of numbered items
15 | * @returns The next number in line as a string
16 | */
17 | export default function getNextNumber(items: string[]): string {
18 | return (Math.max(0, ...items.map(parseNumber)) + 1)
19 | .toString()
20 | .padStart(4, '0');
21 | }
22 |
--------------------------------------------------------------------------------
/src/models/migrationLock/definition.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { DataTypes } from 'sequelize';
3 |
4 | // global
5 | import createAttributes from '@wildebeest/utils/createAttributes';
6 |
7 | /**
8 | * Attribute definitions for the model
9 | */
10 | const attributes = createAttributes({
11 | /** Indicates that the migrator should be locked because another instance is running the migrations. */
12 | isLocked: {
13 | allowNull: false,
14 | defaultValue: false,
15 | type: DataTypes.BOOLEAN,
16 | unique: 'isLocked must be unique',
17 | },
18 | });
19 |
20 | // Model definition
21 | export default {
22 | attributes,
23 | tableName: 'migrationLocks',
24 | };
25 |
--------------------------------------------------------------------------------
/generators/migrationTypes/tableColumnMigration/index.ts:
--------------------------------------------------------------------------------
1 | // local
2 | import { listAllAttributes } from './lib';
3 |
4 | /**
5 | * A higher order generator that creates a Table column migration migrator.
6 | *
7 | * Migrate a table column
8 | */
9 | export default {
10 | parentType: 'tableMigration',
11 | prompts: {
12 | columnName: (repo, { columnSuggestOnly = true }) => ({
13 | message: `What column should be modified? ${
14 | columnSuggestOnly ? '(suggestOnly)' : ''
15 | }`,
16 | source: ({ modelPath }) => listAllAttributes(repo, modelPath),
17 | suggestOnly: columnSuggestOnly,
18 | type: 'autocomplete',
19 | }),
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/generators/migrations/removeColumns/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Remove multiple columns on up, and add them back on down
3 | */
4 | export default {
5 | configure: ({ modelTableName, columns, model }) => ({
6 | name: `remove-columns-${modelTableName}-${columns
7 | .map(({ columnName }) => columnName)
8 | .join('-')}`,
9 | comment: `Remove columns (${columns
10 | .map(({ columnName }) => columnName)
11 | .join(', ')}) to ${model}.`, // eslint-disable-line max-len
12 | }),
13 | description: 'Remove multiple columns on up, and add them back on down',
14 | columnSuggestOnly: true,
15 | withGetRowDefaults: true,
16 | type: 'tableColumnsMigration',
17 | };
18 |
--------------------------------------------------------------------------------
/generators/migrations/removeUniqueConstraintIndex/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Remove a unique constrain across multiple columns on up, and add it back on down
3 | */
4 | export default {
5 | configure: ({ columns, model }) => ({
6 | name: `remove-unique-constraint-index-${columns
7 | .map(({ columnName }) => columnName)
8 | .join('-')}`,
9 | comment: `Remove unique index on ${model} fields: ${columns
10 | .map(({ columnName }) => columnName)
11 | .join(', ')}.`, // eslint-disable-line max-len
12 | }),
13 | description:
14 | 'Remove a unique constrain across multiple columns on up, and add it back on down',
15 | suggestOnly: true,
16 | type: 'tableColumnsMigration',
17 | };
18 |
--------------------------------------------------------------------------------
/generators/migrations/addUniqueConstraintIndex/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Add a unique constrain across multiple columns on up, and remove the constraint on down
3 | */
4 | export default {
5 | configure: ({ columns, model }) => ({
6 | name: `add-unique-constraint-index-${columns
7 | .map(({ columnName }) => columnName)
8 | .join('-')}`,
9 | comment: `Add unique index on ${model} fields: ${columns
10 | .map(({ columnName }) => columnName)
11 | .join(', ')}.`, // eslint-disable-line max-len
12 | }),
13 | description:
14 | 'Add a unique constrain across multiple columns on up, and remove the constraint on down',
15 | skipColumnPrompts: true,
16 | type: 'tableColumnsMigration',
17 | };
18 |
--------------------------------------------------------------------------------
/generators/migrations/changeColumn/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Change a column definition
3 | */
4 | export default {
5 | configure: ({ columnName, model }) => ({
6 | name: `change-column-${columnName}`,
7 | comment: `Change column "${columnName}" on table ${model}`,
8 | }),
9 | description: 'Change a column definition',
10 | prompts: {
11 | oldDefinition: {
12 | message: 'What is the old column definition?',
13 | extension: 'hbs',
14 | type: 'editor',
15 | },
16 | newDefinition: {
17 | message: 'What is the new definition?',
18 | extension: 'hbs',
19 | type: 'editor',
20 | },
21 | },
22 | type: 'tableColumnMigration', // TODO 'tablesColumnMigration',
23 | };
24 |
--------------------------------------------------------------------------------
/generators/migrationTypes/tableColumnsMigration/regexs.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Detect if an attribute definition has a validator
3 | */
4 | export const ATTRIBUTE_VALIDATOR_REGEX = /validate: ([A-Z_].+?),/;
5 |
6 | /**
7 | * Create an attribute regex
8 | *
9 | * @param name - The name of the attribute
10 | * @param exportType - The manner in which the attribute is exported (i.e. export const )
11 | * @returns The regex for matching comment and definition from an attribute
12 | */
13 | export const ATTRIBUTE_VALUE_REGEX = (
14 | name: string,
15 | exportType: string,
16 | ): RegExp =>
17 | new RegExp(
18 | `\\/\\*\\*\\n \\* (.+?)\\n \\*[\\s\\S]+${exportType}${name}: ModelAttributeColumnOptions = {([\\s\\S]+?)};`,
19 | );
20 |
--------------------------------------------------------------------------------
/generators/migrationTypes/dbMigration/index.ts:
--------------------------------------------------------------------------------
1 | // commons
2 | import getNextNumber from '@wildebeest/utils/getNextNumber';
3 |
4 | // local
5 | import actions from './actions';
6 |
7 | /**
8 | * A higher order generator that creates a Db migration
9 | *
10 | * A database migration
11 | */
12 | export default {
13 | actions,
14 | configure: ({ location, generatorName }, repo) => ({
15 | migrationNumber: getNextNumber(repo.getEntryFolder(location)),
16 | templateName: repo.hasTemplate(generatorName)
17 | ? generatorName
18 | : 'db-migration',
19 | }),
20 | location: 'migrations/migrations',
21 | locationIsFolder: true,
22 | mode: 'backend',
23 | skipActions: true,
24 | requiredProps: [],
25 | };
26 |
--------------------------------------------------------------------------------
/src/utils/logSection.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import Logger from '@wildebeest/Logger';
3 |
4 | /**
5 | * Run some code wrapped in a logged header
6 | *
7 | * @param logger - The logger to use
8 | * @param cb - The code to run
9 | * @param header - The header of the log section
10 | * @returns The section execution promise wrapped in logging
11 | */
12 | export default async function logSection(
13 | logger: Logger,
14 | cb: () => any, // eslint-disable-line @typescript-eslint/no-explicit-any
15 | header = 'Migrations',
16 | ): Promise {
17 | // Log start
18 | logger.divide();
19 | logger.info(`${header}:\n`);
20 |
21 | // Run the cb
22 | await Promise.resolve(cb());
23 |
24 | // Log finish
25 | logger.divide();
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/tableExists.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { QueryTypes, Sequelize, Transaction } from 'sequelize';
3 |
4 | /**
5 | * Check if the model table exists
6 | *
7 | * @param db - The db to check against
8 | * @param tableName - The name of the table to check
9 | * @param transaction - The current transaction
10 | * @returns True if the table exists
11 | */
12 | export default async function tableExists(
13 | db: Sequelize,
14 | tableName: string,
15 | transaction?: Transaction,
16 | ): Promise {
17 | return db
18 | .query(
19 | `SELECT * FROM pg_catalog.pg_tables WHERE tablename='${tableName}' `,
20 | { type: QueryTypes.SELECT, transaction },
21 | )
22 | .then((tables) => tables.length === 1);
23 | }
24 |
--------------------------------------------------------------------------------
/generators/migrationTypes/tableAssociationColumn/index.ts:
--------------------------------------------------------------------------------
1 | // local
2 | import { getAssociations } from './lib';
3 |
4 | /**
5 | * A higher order generator that creates a Table association column migrator.
6 | *
7 | * A migrator for table column that is an association
8 | */
9 | export default {
10 | parentType: 'tableMigration',
11 | prompts: {
12 | columnName: (repo) => ({
13 | message: 'What column should be modified? (suggestOnly)',
14 | source: ({ modelPath }) =>
15 | getAssociations(modelPath, repo)
16 | .filter(({ associationType }) => associationType === 'belongsTo')
17 | .map(({ name }) => `${name}Id`),
18 | suggestOnly: true,
19 | type: 'autocomplete',
20 | }),
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/src/utils/reverseMigrator.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { MigrationDefinition, ModelMap } from '@wildebeest/types';
3 |
4 | /**
5 | * Reverse a higher order generator so down returns up and up returns down
6 | *
7 | * @param createMigrator - The higher order migrator to reverse
8 | * @returns The higher order migrator reversed
9 | */
10 | export default function reverseMigrator(
11 | createMigrator: (options: TOptions) => MigrationDefinition,
12 | ): (options: TOptions) => MigrationDefinition {
13 | return (options: TOptions): MigrationDefinition => {
14 | const { up, down } = createMigrator(options);
15 | return {
16 | down: up,
17 | up: down,
18 | };
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/generators/migrations/addIndex/index.ts:
--------------------------------------------------------------------------------
1 | // commons
2 | import { IndexMethod } from '@wildebeest/types';
3 |
4 | /**
5 | * Add a new index to a table
6 | */
7 | export default {
8 | configure: ({ columns, model }) => ({
9 | name: `add-index-${columns.map(({ columnName }) => columnName).join('-')}`,
10 | comment: `Add index on ${model} fields: ${columns
11 | .map(({ columnName }) => columnName)
12 | .join(', ')}.`,
13 | }),
14 | description: 'Add a new index to a table',
15 | prompts: {
16 | method: {
17 | default: IndexMethod.BTree,
18 | message: 'What method of index?',
19 | source: () => Object.values(IndexMethod),
20 | type: 'autocomplete',
21 | },
22 | },
23 | type: 'tableColumnsMigration',
24 | };
25 |
--------------------------------------------------------------------------------
/generators/migrationTypes/tableAssociationColumn/lib.ts:
--------------------------------------------------------------------------------
1 | // commons
2 | import listAssociations from '@commons/utils/listAssociations';
3 |
4 | // utils
5 | import modelPathToAssociationPath from '@generators/utils/modelPathToAssociationPath';
6 |
7 | /**
8 | * Get the associations from a modelPath
9 | *
10 | * @param modelPath - The path to the model definition index file
11 | * @param repo - The repository configuration
12 | * @returns The associations
13 | */
14 | export const getAssociations = function getAssociations(
15 | modelPath,
16 | repo,
17 | ): AssociationConfig[] {
18 | const loc = modelPathToAssociationPath(modelPath);
19 | if (!repo.hasEntryFile(loc)) {
20 | return [];
21 | }
22 | return listAssociations(repo.getEntryFile(loc));
23 | };
24 |
--------------------------------------------------------------------------------
/generators/migrations/renameConstraint/index.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import kebabCase from 'lodash/kebabCase';
3 |
4 | /**
5 | * Rename a table column constraint
6 | */
7 | export default {
8 | configure: ({ model, columnName, newName, oldName }) => ({
9 | name: `rename-constraint-${kebabCase(newName)}`,
10 | comment: `Rename the constraint ${oldName} for ${model}.${columnName} to ${newName}.`,
11 | }),
12 | description: 'Rename a table column constraint',
13 | prompts: {
14 | oldName: {
15 | message: 'What was the old name of the constraint?',
16 | type: 'name',
17 | },
18 | newName: {
19 | message: 'What is the new name of the constraint?',
20 | type: 'name',
21 | },
22 | },
23 | type: 'tableColumnMigration',
24 | };
25 |
--------------------------------------------------------------------------------
/generators/migrationTypes/tablesColumnMigration/index.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import intersection from 'lodash/intersection';
3 |
4 | // migrationTypes
5 | import { listAllAttributes } from '../tableColumnMigration/lib';
6 |
7 | /**
8 | * A higher order generator that creates a Tables column migration migrator.
9 | *
10 | * Migrate the same column on multiple tables
11 | */
12 | export default {
13 | parentType: 'tablesMigration',
14 | prompts: {
15 | columnName: (repo) => ({
16 | message: 'What column should be modified? (suggestOnly)',
17 | source: ({ tables }) =>
18 | intersection(
19 | tables.map(({ modelPath }) => listAllAttributes(repo, modelPath)),
20 | ),
21 | suggestOnly: true,
22 | type: 'autocomplete',
23 | }),
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/src/utils/invert.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Invert an object so that the values look up the keys.
3 | * If the object has an array as the value, each item in the array will be inverted.
4 | *
5 | * @param obj - The object to invert
6 | * @returns The inverted object
7 | */
8 | export default function invert(
9 | obj: { [key in string]: string | string[] },
10 | ): { [key in string]: string } {
11 | const result: { [key in string]: string } = {};
12 |
13 | // Invert
14 | Object.keys(obj).forEach((key: string) => {
15 | const instance = obj[key];
16 | if (typeof instance === 'string') {
17 | result[instance] = key;
18 | } else {
19 | (obj[key] as string[]).forEach((listKey: string) => {
20 | result[listKey] = key;
21 | });
22 | }
23 | });
24 | return result;
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/getKeys.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { StringKeys } from '@wildebeest/types';
3 |
4 | /**
5 | * Object.keys for string keys only
6 | *
7 | * @param o - The object to get the keys from
8 | * @returns The string keys of the object preserving type
9 | */
10 | export function getStringKeys(
11 | o: T,
12 | ): StringKeys[] {
13 | return Object.keys(o).filter((k) => typeof k === 'string') as StringKeys[];
14 | }
15 |
16 | /**
17 | * Object.keys that actually preserves keys as types.
18 | *
19 | * @param o - The object to get the keys from
20 | * @returns The keys of the object preserving type
21 | */
22 | export default function getKeys(
23 | o: T,
24 | ): (keyof T)[] {
25 | return Object.keys(o) as (keyof T)[];
26 | }
27 |
--------------------------------------------------------------------------------
/generateMixins/helpers/getKeys.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { StringKeys } from '../types';
3 |
4 | /**
5 | * Object.keys for string keys only
6 | *
7 | * @param o - The object to get the keys from
8 | * @returns The string keys of the object preserving type
9 | */
10 | export function getStringKeys(
11 | o: T,
12 | ): StringKeys[] {
13 | return Object.keys(o).filter((k) => typeof k === 'string') as StringKeys[];
14 | }
15 |
16 | /**
17 | * Object.keys that actually preserves keys as types.
18 | *
19 | * @param o - The object to get the keys from
20 | * @returns The keys of the object preserving type
21 | */
22 | export default function getKeys(
23 | o: T,
24 | ): (keyof T)[] {
25 | return Object.keys(o) as (keyof T)[];
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/getForeignKeyName.ts:
--------------------------------------------------------------------------------
1 | // db
2 | import { Association } from '@wildebeest/types';
3 |
4 | /**
5 | * Get the name of the foreign key if it is provided
6 | *
7 | * @param association - The association to get the foreign key name from
8 | * @param defaultValue - When none is provided, fall back to this
9 | * @returns The name of the foreign key or null if not provided
10 | */
11 | export default function getForeignKeyName(
12 | association: Association,
13 | defaultValue = 'id',
14 | ): string {
15 | if (typeof association === 'string' || !association.foreignKey) {
16 | return defaultValue;
17 | }
18 | if (typeof association.foreignKey === 'string') {
19 | return association.foreignKey;
20 | }
21 | return association.foreignKey.name || defaultValue;
22 | }
23 |
--------------------------------------------------------------------------------
/generators/migrations/renameIndex/index.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { NOT_EMPTY_REGEX } from '@generators/regexes';
3 |
4 | /**
5 | * Rename an index
6 | */
7 | export default {
8 | configure: ({ oldName, newName }) => ({
9 | name: `rename-index-to-${newName}`,
10 | comment: `Rename the index ${oldName} to ${newName}.`,
11 | }),
12 | description: 'Rename an index',
13 | prompts: {
14 | oldName: {
15 | message: 'What is the old index name?',
16 | type: 'input',
17 | validate: (value) =>
18 | NOT_EMPTY_REGEX.test(value) ? true : 'oldName is required',
19 | },
20 | newName: {
21 | message: 'What is the new name?',
22 | type: 'input',
23 | validate: (value) =>
24 | NOT_EMPTY_REGEX.test(value) ? true : 'newName is required',
25 | },
26 | },
27 | type: 'dbMigration',
28 | };
29 |
--------------------------------------------------------------------------------
/generators/migrationTypes/index.ts:
--------------------------------------------------------------------------------
1 | // local
2 | import alterEnum from './alterEnum';
3 | import changeEnumAttributes from './changeEnumAttributes';
4 | import dbMigration from './dbMigration';
5 | import tableAssociationColumn from './tableAssociationColumn';
6 | import tableColumnMigration from './tableColumnMigration';
7 | import tableColumnsMigration from './tableColumnsMigration';
8 | import tableMigration from './tableMigration';
9 | import tablesColumnMigration from './tablesColumnMigration';
10 | import tablesMigration from './tablesMigration';
11 |
12 | /**
13 | * Types of migration generators
14 | */
15 | export default {
16 | alterEnum,
17 | changeEnumAttributes,
18 | dbMigration,
19 | tableAssociationColumn,
20 | tableColumnMigration,
21 | tableColumnsMigration,
22 | tableMigration,
23 | tablesColumnMigration,
24 | tablesMigration,
25 | };
26 |
--------------------------------------------------------------------------------
/generators/migrations/renameEnum/index.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { NOT_EMPTY_REGEX } from '@generators/regexes';
3 |
4 | /**
5 | * Rename an enum from one value to another
6 | */
7 | export default {
8 | configure: ({ oldName, columnName, modelTableName, model }) => {
9 | const newName = `enum_${modelTableName}_${columnName}`;
10 | return {
11 | name: `rename-${oldName}-to-${newName}`,
12 | newName,
13 | comment: `Rename the enum definition for ${model}.${columnName} to ${newName}`,
14 | };
15 | },
16 | description: 'Rename an enum from one value to another',
17 | prompts: {
18 | oldName: {
19 | message: 'What was the old enum name?',
20 | type: 'input',
21 | validate: (value) =>
22 | NOT_EMPTY_REGEX.test(value) ? true : 'oldName is required',
23 | },
24 | },
25 | type: 'tableColumnMigration',
26 | };
27 |
--------------------------------------------------------------------------------
/generateMixins/helpers/indexPaths.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { CompilerOptions } from 'typescript';
3 |
4 | /**
5 | * Get the typescript configuration in the directory
6 | *
7 | * @param compilerOptions - The projects compiler options
8 | * @returns Mapping from path prefix to actual location, used to resolve aliased paths
9 | */
10 | export default function indexPaths(
11 | compilerOptions: CompilerOptions,
12 | ): { [k in string]: string } {
13 | const { paths = {} } = compilerOptions;
14 | return Object.keys(paths).reduce((map, key) => {
15 | const replaceWith = paths[key][0];
16 | // No true wildcard support, just assuming that there is always 1 wildcard and it's '/*'
17 | // No support for multiple paths per key, either.
18 | const keyPrefix = key.split('/*')[0];
19 | const path = replaceWith.split('/*')[0];
20 | return { ...map, [keyPrefix]: path };
21 | }, {});
22 | }
23 |
--------------------------------------------------------------------------------
/generateMixins/Handlebars.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Creates the handlebars instance
3 | */
4 |
5 | // external
6 | import Handlebars from 'handlebars';
7 |
8 | // helpers
9 | import pascalCase from './helpers/pascalCase';
10 |
11 | // Set the helpers
12 | Handlebars.registerHelper('pascalCase', pascalCase);
13 | Handlebars.registerHelper('pad', (word: string) => '/'.repeat(word.length));
14 | Handlebars.registerHelper('ifNotEqual', function ifNotEqual(
15 | this: any,
16 | arg1: string,
17 | arg2: string,
18 | options: Handlebars.HelperOptions,
19 | ) {
20 | return arg1 !== arg2 ? options.fn(this) : options.inverse(this);
21 | });
22 |
23 | Handlebars.registerHelper('ifEqual', function ifNotEqual(
24 | this: any,
25 | arg1: string,
26 | arg2: string,
27 | options: Handlebars.HelperOptions,
28 | ) {
29 | return arg1 === arg2 ? options.fn(this) : options.inverse(this);
30 | });
31 |
32 | export default Handlebars;
33 |
--------------------------------------------------------------------------------
/generators/migrationTypes/changeEnumAttributes/lib.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Get the attributes of an enum
3 | */
4 | export const ENUM_ATTRIBUTE_REGEX = /([a-zA-Z0-9]*)(:| =) '([a-zA-Z0-9]*)',/g;
5 |
6 | /**
7 | * List the attributes of an enum from the content definition
8 | *
9 | * @param content - The enum content
10 | * @param returnValue - Return the enum value (false means return the key)
11 | * @returns The enum attributes
12 | */
13 | export const listEnumAttributes = function listEnumAttributes(
14 | content: string,
15 | returnValue = true,
16 | ): string[] {
17 | const attributes = [];
18 | let attr = ENUM_ATTRIBUTE_REGEX.exec(content);
19 | while (attr) {
20 | // Save the key or value
21 | const [, key, , value] = attr;
22 | attributes.push(returnValue ? value : key);
23 |
24 | // Get the next
25 | attr = ENUM_ATTRIBUTE_REGEX.exec(content);
26 | }
27 | return attributes;
28 | };
29 |
--------------------------------------------------------------------------------
/generateMixins/helpers/index.ts:
--------------------------------------------------------------------------------
1 | // local
2 | export { default as associationDefinitionToSections } from './associationDefinitionToSections';
3 | export { default as calculateMixinAttributes } from './calculateMixinAttributes';
4 | export { default as createBase } from './createBase';
5 | export { default as createCompilerHost } from './createCompilerHost';
6 | export { default as extractAssociationDefinitions } from './extractAssociationDefinitions';
7 | export { default as determineModelToExtend } from './determineModelToExtend';
8 | export { default as getCompilerOptions } from './getCompilerOptions';
9 | export { default as getKeys } from './getKeys';
10 | export { default as indexPaths } from './indexPaths';
11 | export { default as pascalCase } from './pascalCase';
12 | export { default as serializeAssociationType } from './serializeAssociationType';
13 | export { default as setConfigDefaults } from './setConfigDefaults';
14 |
--------------------------------------------------------------------------------
/generators/migrations/rename-enum-value/index.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { NOT_EMPTY_REGEX } from '@generators/regexs';
3 |
4 | /**
5 | * Rename an enum value in place
6 | */
7 | export default {
8 | configure: ({ enumName, oldValue, newValue }) => ({
9 | name: `rename-enum-${enumName}-value-${oldValue}-to-${newValue}`,
10 | comment: `Rename ${oldValue} to ${newValue} on enum ${enumName}`,
11 | }),
12 | description: 'Rename an enum value in place',
13 | type: 'alterEnum',
14 | prompts: {
15 | oldValue: {
16 | message: 'What is the old value?',
17 | type: 'input',
18 | validate: (value) =>
19 | NOT_EMPTY_REGEX.test(value) ? true : 'oldValue is required',
20 | },
21 | newValue: {
22 | message: 'What is the new value?',
23 | type: 'input',
24 | validate: (value) =>
25 | NOT_EMPTY_REGEX.test(value) ? true : 'newValue is required',
26 | },
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/src/models/migration/definition.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { DataTypes } from 'sequelize';
3 |
4 | // global
5 | import createAttributes from '@wildebeest/utils/createAttributes';
6 |
7 | // local
8 | import hooks from './hooks';
9 |
10 | /**
11 | * Attribute definitions for the model
12 | */
13 | const attributes = createAttributes({
14 | /** The id is an integer */
15 | id: {
16 | autoIncrement: true,
17 | primaryKey: true,
18 | type: DataTypes.INTEGER,
19 | },
20 | /** The batch the migration was run in */
21 | batch: {
22 | allowNull: true,
23 | type: DataTypes.INTEGER,
24 | },
25 | /** The name of the migration that has been run */
26 | name: {
27 | allowNull: false,
28 | type: DataTypes.STRING,
29 | unique: 'name must be unique',
30 | },
31 | });
32 |
33 | // Model definition
34 | export default {
35 | attributes,
36 | options: { hooks },
37 | tableName: 'migrations',
38 | };
39 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "module": "commonjs",
5 | "lib": ["esnext", "dom", "es2015"],
6 | "declaration": true,
7 | "declarationMap": true,
8 | "sourceMap": true,
9 | "removeComments": false,
10 | "esModuleInterop": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "noImplicitReturns": true,
14 | "noFallthroughCasesInSwitch": true,
15 | "moduleResolution": "node",
16 | "baseUrl": ".",
17 | "paths": {
18 | "@generators/*": ["./generators*"],
19 | "@wildebeest*": ["./src*"]
20 | },
21 | "outDir": "./build",
22 | "types": ["node", "mocha"],
23 | "strict": true
24 | },
25 | "include": [
26 | "src/**/*",
27 | "scripts/**/*",
28 | "generateMixins/**/*"
29 | // "generators/**/*" TODO
30 | ],
31 | "exclude": ["node_modules", "build", "docs", "Internal-Documentation"]
32 | }
33 |
--------------------------------------------------------------------------------
/src/utils/expectedColumnNames.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { Associations, Attributes } from '@wildebeest/types';
3 |
4 | // local
5 | import getForeignKeyName from './getForeignKeyName';
6 |
7 | /**
8 | * Determine the expected column names for a model definition
9 | *
10 | * @param attributes - The sequelize db model attribute definitions
11 | * @param associations - The associations for that db model
12 | * @returns The expected column names
13 | */
14 | export default function expectedColumnNames(
15 | attributes: Attributes = {},
16 | associations: Associations = {},
17 | ): string[] {
18 | const { belongsTo = {} } = associations;
19 | return [
20 | // The attributes
21 | ...Object.keys(attributes),
22 | // The belongs to columns
23 | ...Object.keys(belongsTo).map((associationName) =>
24 | getForeignKeyName(belongsTo[associationName], `${associationName}Id`),
25 | ),
26 | ];
27 | }
28 |
--------------------------------------------------------------------------------
/src/migrationTypes/init.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { MigrationDefinition, ModelMap } from '@wildebeest/types';
3 |
4 | // local
5 | import custom from './custom';
6 |
7 | /**
8 | * Options for init migration
9 | */
10 | export type InitMigrationOptions = {
11 | /** The genesis migration to begin from TODO this should be optional and a genesis can be created with lock/migration tables */
12 | genesisSchema: string;
13 | };
14 |
15 | /**
16 | * The initial migration to begin all migrations
17 | *
18 | * @param options - Init migration options
19 | * @returns The init migrator
20 | */
21 | export default function init({
22 | genesisSchema,
23 | }: InitMigrationOptions): MigrationDefinition {
24 | return custom({
25 | up: (_, wildebeest) =>
26 | wildebeest.runWithLock((lock) => lock.restoreSchema(genesisSchema)),
27 | down: (_, wildebeest) => wildebeest.runWithLock((lock) => lock.dropAll()),
28 | });
29 | }
30 |
--------------------------------------------------------------------------------
/generators/migrations/changeEnumColumn/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Migrate an enum column
3 | */
4 | export default {
5 | configure: ({ columnName, modelTableName, model }) => ({
6 | name: `change-enum-column-${columnName}-${modelTableName}`,
7 | comment: `Change enum column ${columnName} on table ${model}`,
8 | }),
9 | description: 'Migrate an enum column',
10 | prompts: {
11 | oldAttributes: {
12 | message: 'What are the old enum attributes?',
13 | extension: 'hbs',
14 | type: 'editor',
15 | },
16 | newAttributes: {
17 | message: 'What are the new enum attributes?',
18 | extension: 'hbs',
19 | type: 'editor',
20 | },
21 | oldDefault: {
22 | message: 'What is the old default value?',
23 | type: 'input',
24 | },
25 | newDefault: {
26 | message: 'What is the new default value?',
27 | type: 'input',
28 | },
29 | },
30 | type: 'tableColumnMigration', // TODO tablesColumnMigration
31 | };
32 |
--------------------------------------------------------------------------------
/src/utils/getAssociationColumnName.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { Association } from '@wildebeest/types';
3 |
4 | /**
5 | * Get the name of the column for a belongsTo association
6 | *
7 | * @param association - The association config
8 | * @param name - The name of the association
9 | * @returns The name of the column
10 | */
11 | export default function getAssociationColumnName(
12 | association: Association,
13 | associationName: string,
14 | ): string {
15 | // Return name when specified
16 | if (
17 | typeof association === 'object' &&
18 | typeof association.foreignKey === 'string'
19 | ) {
20 | return association.foreignKey;
21 | }
22 | if (
23 | typeof association === 'object' &&
24 | typeof association.foreignKey === 'object' &&
25 | association.foreignKey.name
26 | ) {
27 | return association.foreignKey.name;
28 | }
29 |
30 | // Return the default
31 | return `${associationName}Id`;
32 | }
33 |
--------------------------------------------------------------------------------
/generators/migrations/renameColumn/index.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { NOT_EMPTY_REGEX } from '@generators/regexs';
3 |
4 | /**
5 | * Create a migration definition that renames a column
6 | */
7 | export default {
8 | configure: ({ modelTableName, newName, oldName, model }) => ({
9 | name: `rename-column-${modelTableName}-to-${newName}`,
10 | comment: `Rename the column ${oldName} to ${newName} on table ${model}`,
11 | }),
12 | description: 'Create a migration definition that renames a column',
13 | prompts: {
14 | oldName: {
15 | message: 'What was the old column name?',
16 | type: 'input',
17 | validate: (value) =>
18 | NOT_EMPTY_REGEX.test(value) ? true : 'oldName is required',
19 | },
20 | newName: {
21 | message: 'What is the new column name?',
22 | type: 'input',
23 | validate: (value) =>
24 | NOT_EMPTY_REGEX.test(value) ? true : 'newName is required',
25 | },
26 | },
27 | type: 'tableMigration',
28 | };
29 |
--------------------------------------------------------------------------------
/src/utils/listColumns.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | // external
3 | import { QueryTypes } from 'sequelize';
4 |
5 | // global
6 | import WildebeestDb from '@wildebeest/classes/WildebeestDb';
7 | import { ModelMap } from '@wildebeest/types';
8 |
9 | /**
10 | * The top level metadata for a column in the db
11 | */
12 | export type ListedColumn = {
13 | /** Name of column */
14 | column_name: string;
15 | /** Type of column */
16 | data_type: string;
17 | };
18 |
19 | /**
20 | * List the columns that the table currently has
21 | *
22 | * @returns The current table columns
23 | */
24 | export default function listColumns(
25 | db: WildebeestDb,
26 | tableName: string,
27 | ): Promise {
28 | return db
29 | .query(
30 | `SELECT column_name,data_type from information_schema.columns WHERE table_name='${tableName}'`,
31 | { type: QueryTypes.SELECT },
32 | )
33 | .then((columns) =>
34 | (columns as ListedColumn[]).map(({ column_name }) => column_name),
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/views/index.hbs:
--------------------------------------------------------------------------------
1 | Database Migrations
2 |
3 |
4 |
5 |
6 | {{#each migrations}}
7 | -
8 |
9 | {{#if isRun }}
10 | DOWN
11 | {{else}}
12 | UP
13 | {{/if}}
14 |
15 | {{name}}
16 |
17 |
18 | {{/each}}
19 |
20 |
21 |
22 |
23 | {{#if hasBackPage}}
24 | Back
25 | {{/if}}
26 | {{#if hasNextPage}}
27 | Next
28 | {{/if}}
29 |
30 |
--------------------------------------------------------------------------------
/src/utils/dropEnum.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import WildebeestDb from '@wildebeest/classes/WildebeestDb';
3 | import {
4 | Attributes,
5 | MigrationTransactionOptions,
6 | ModelMap,
7 | } from '@wildebeest/types';
8 |
9 | /**
10 | * Drop an enum from the db
11 | *
12 | * @param db - The database to drop the enum from
13 | * @param name - The name of the enum in the db to remove
14 | * @param rawTransactionOptions - The existing transaction
15 | * @param cascade - Cascade result to other references
16 | * @returns The drop enum promise
17 | */
18 | export default async function dropEnum<
19 | TModels extends ModelMap,
20 | TAttributes extends Attributes
21 | >(
22 | db: WildebeestDb,
23 | name: string,
24 | transactionOptions?: MigrationTransactionOptions,
25 | cascade?: boolean,
26 | ): Promise {
27 | // Raw query interface
28 | const { queryInterface } = db;
29 |
30 | // Drop the enum
31 | await queryInterface.sequelize.query(
32 | `DROP type IF EXISTS "${name}" ${cascade ? 'CASCADE' : ''};`,
33 | transactionOptions,
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/utils/restoreFromDump.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { execSync } from 'child_process';
3 |
4 | // global
5 | import Wildebeest from '@wildebeest/classes/Wildebeest';
6 | import { ModelMap } from '@wildebeest/types';
7 |
8 | /**
9 | * Restore the database from a pd_dump output
10 | *
11 | * @param wildebeest - The wildebeest instance to restore a dump to
12 | * @param name - The name of the schema that was written
13 | * @param schemaPath - The path to the schema folder
14 | * @returns The pg_restore promise
15 | */
16 | export default async function restoreFromDump(
17 | wildebeest: Wildebeest,
18 | name: string,
19 | ): Promise {
20 | if (!wildebeest.schemaExists(name)) {
21 | throw new Error(`Schema definition not found: "${name}"`);
22 | }
23 | await Promise.resolve(
24 | execSync(
25 | `pg_restore -d "${
26 | wildebeest.db.databaseUri
27 | }" -n public -C ${wildebeest.getSchemaFile(name)}`,
28 | ),
29 | );
30 | // Log success
31 | wildebeest.logger.warn(`\nRestored database schema dump: "${name}"\n`);
32 | }
33 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Migration files for migrating the db using Sequelize and Umzug
3 | */
4 |
5 | // external
6 | import { DataTypes } from 'sequelize';
7 |
8 | // local
9 | import * as checks from './checks';
10 | import Wildebeest, {
11 | NamingConventions,
12 | WildebeestOptions,
13 | } from './classes/Wildebeest';
14 | import { CASCADE_HOOKS, NON_NULL } from './constants';
15 | import * as utils from './utils';
16 |
17 | // expose all migration types and classes
18 | export * from './classes';
19 | export * from './migrationTypes';
20 |
21 | // Types
22 | export * from './models';
23 | export * from './mixins/types';
24 | export * from './types';
25 | export { default as Logger } from './Logger';
26 | export const {
27 | createAssociationApply,
28 | createHooks,
29 | createAssociationApplyValue,
30 | createAttributes,
31 | escapeJson,
32 | } = utils;
33 | export {
34 | DataTypes,
35 | checks,
36 | utils,
37 | CASCADE_HOOKS,
38 | NON_NULL,
39 | Wildebeest,
40 | NamingConventions,
41 | WildebeestOptions,
42 | };
43 |
44 | // Default is wildebeest class
45 | export default Wildebeest;
46 |
--------------------------------------------------------------------------------
/generateMixins/helpers/getCompilerOptions.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import * as fs from 'fs';
3 | import { join } from 'path';
4 | import { CompilerOptions } from 'typescript';
5 |
6 | /**
7 | * Get the typescript configuration in the directory
8 | *
9 | * @param directory - The directory to look for the tsconfig in
10 | * @param tsConfigPath - The path to the tsconfig if not in the expected tsconfig.json location
11 | * @returns The tsconfig or throws an error
12 | */
13 | export default function getCompilerOptions(
14 | directory: string,
15 | tsConfigPath = join(directory, 'tsconfig.json'),
16 | ): CompilerOptions {
17 | // ensure the file exists
18 | if (!fs.existsSync(tsConfigPath)) {
19 | throw new Error(`Path to tsconfig does not exist: "${tsConfigPath}"`);
20 | }
21 |
22 | // Remove single-line comments.
23 | // Comments are allowed in tsconfig.json, but not allowed in standard json format, which `JSON.parse` expects.
24 | const tscJson = fs
25 | .readFileSync(tsConfigPath, 'utf-8')
26 | .replace(/\/\/.*?\n|\/\* .*?\n/g, '');
27 |
28 | // Parse to JSON
29 | return JSON.parse(tscJson).compilerOptions as CompilerOptions;
30 | }
31 |
--------------------------------------------------------------------------------
/src/utils/indexConstraints.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import keyBy from 'lodash/keyBy';
3 |
4 | /**
5 | * Foreign key constraint definition for a table column
6 | */
7 | export type ColumnConstraint = {
8 | /** The name of the column */
9 | columnName: string;
10 | /** The name of foreign the table */
11 | tableName?: string;
12 | /** The name of the foreign key column name */
13 | foreignColumnName?: string;
14 | };
15 |
16 | /**
17 | * Shorthand constraint input is to specify columnName only and use the default table name (removing `Id`)
18 | */
19 | export type RawConstraint = string | ColumnConstraint;
20 |
21 | /**
22 | * Index foreign key constraints by name of column
23 | */
24 | export default function indexConstraints(
25 | constraints: RawConstraint[],
26 | ): { [columnName in string]: ColumnConstraint } {
27 | // Index constraints so the can be looked up by columnName
28 | const columnConstraints: ColumnConstraint[] = constraints.map((constraint) =>
29 | typeof constraint === 'string'
30 | ? {
31 | columnName: constraint,
32 | }
33 | : constraint,
34 | );
35 | return keyBy(columnConstraints, 'columnName');
36 | }
37 |
--------------------------------------------------------------------------------
/src/utils/getAssociationAttribute.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import * as sequelize from 'sequelize';
3 |
4 | // global
5 | import { Association } from '@wildebeest/types';
6 |
7 | /**
8 | * Get the attribute definition for an association column
9 | *
10 | * @param association - The association config
11 | * @param config - The attribute config of the column to join on
12 | * @returns The association attribute definition
13 | */
14 | export default function getAssociationAttribute(
15 | association: Association,
16 | config: sequelize.ModelAttributeColumnOptions = {
17 | defaultValue: sequelize.UUIDV4,
18 | type: sequelize.UUID,
19 | },
20 | ): sequelize.ModelAttributeColumnOptions {
21 | // We do not copy over some values
22 | const { primaryKey, allowNull, unique, defaultValue, ...rest } = config; // eslint-disable-line @typescript-eslint/no-unused-vars,max-len
23 | return {
24 | ...rest,
25 | allowNull: !!(
26 | typeof association === 'object' &&
27 | (!association.foreignKey ||
28 | (typeof association.foreignKey === 'object' &&
29 | association.foreignKey.allowNull))
30 | ),
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/src/utils/listIndexNames.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { QueryTypes } from 'sequelize';
3 |
4 | // global
5 | import WildebeestDb from '@wildebeest/classes/WildebeestDb';
6 | import { MigrationTransactionOptions, ModelMap } from '@wildebeest/types';
7 |
8 | /**
9 | * A db index
10 | */
11 | export type PgIndex = {
12 | /** The name of the index */
13 | indexname: string;
14 | };
15 |
16 | /**
17 | * List indexes related to a table
18 | *
19 | * @param db - The db instance to check
20 | * @param tableName - The table to list indexes for
21 | * @returns The names of the indexes
22 | */
23 | export default async function listIndexNames(
24 | db: WildebeestDb,
25 | tableName: string,
26 | transactionOptions?: MigrationTransactionOptions,
27 | ): Promise {
28 | const { queryInterface } = db;
29 | // Determine the indexes
30 | const indexes: PgIndex[] = await queryInterface.sequelize.query(
31 | `select * from pg_indexes where tableName='${tableName}'`,
32 | {
33 | ...transactionOptions,
34 | type: QueryTypes.SELECT,
35 | },
36 | );
37 | return indexes.map(({ indexname }) => indexname);
38 | }
39 |
--------------------------------------------------------------------------------
/src/utils/columnAllowsNull.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { QueryTypes } from 'sequelize';
3 |
4 | // global
5 | import WildebeestDb from '@wildebeest/classes/WildebeestDb';
6 | import { MigrationTransactionOptions, ModelMap } from '@wildebeest/types';
7 |
8 | /**
9 | * Check if a postgres column allows null values
10 | *
11 | * @param db - The db instance to use
12 | * @param tableName - The name of the table to check
13 | * @param columnName - The name of the column
14 | * @returns True if null values are allowed
15 | */
16 | export default async function columnAllowsNull(
17 | db: WildebeestDb,
18 | tableName: string,
19 | columnName: string,
20 | transactionOptions?: MigrationTransactionOptions,
21 | ): Promise {
22 | const [row] = await db.queryInterface.sequelize.query(
23 | `
24 | SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT
25 | FROM INFORMATION_SCHEMA.COLUMNS
26 | WHERE table_name = '${tableName}' AND is_nullable = 'YES' AND column_name = '${columnName}'
27 | `,
28 | {
29 | type: QueryTypes.SELECT,
30 | ...transactionOptions,
31 | },
32 | );
33 | return !!row;
34 | }
35 |
--------------------------------------------------------------------------------
/generators/migrations/changeOnDelete/index.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import kebabCase from 'lodash/kebabCase';
3 |
4 | // global
5 | import { OnDelete } from '@wildebeest/types';
6 |
7 | /**
8 | * Change the onDelete status of a foreign key constraint
9 | */
10 | export default {
11 | configure: ({
12 | modelTableName,
13 | model,
14 | columnName,
15 | newOnDelete,
16 | oldOnDelete,
17 | }) => ({
18 | name: `change-on-delete-${modelTableName}-${columnName}-to-${kebabCase(
19 | newOnDelete,
20 | )}`,
21 | comment: `Change the foreign key constraint for column ${model}.${columnName} from ${oldOnDelete} to ${newOnDelete}`,
22 | }),
23 | description: 'Change the onDelete status of a foreign key constraint',
24 | prompts: {
25 | oldOnDelete: {
26 | message: 'What was the old onDelete value?',
27 | source: () => Object.values(OnDelete),
28 | type: 'autocomplete',
29 | },
30 | newOnDelete: {
31 | message: 'What is the new onDelete value?',
32 | source: ({ oldOnDelete }) =>
33 | Object.values(OnDelete).filter((val) => val !== oldOnDelete),
34 | type: 'autocomplete',
35 | },
36 | },
37 | type: 'tableColumnMigration',
38 | };
39 |
--------------------------------------------------------------------------------
/generateMixins/ChangeCase.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import camelCase from 'lodash/camelCase';
3 | import pluralize from 'pluralize';
4 |
5 | // helpers
6 | import pascalCase from './helpers/pascalCase';
7 |
8 | /**
9 | * A class with all of the casings
10 | */
11 | export default class ChangeCase {
12 | /** The word itself (identity function) */
13 | public plain: string;
14 |
15 | /** Casing for camelCase */
16 | public camelCase: string;
17 |
18 | /** Casing for pascalCase */
19 | public pascalCase: string;
20 |
21 | /** Casing for pluralCase */
22 | public pluralCase: string;
23 |
24 | /** Casing for pascalPluralCase */
25 | public pascalPluralCase: string;
26 |
27 | /**
28 | * Create a new case changed
29 | *
30 | * @param word - The word to change the case of
31 | * @param pluralCase - A function that will pluralize a word
32 | */
33 | public constructor(
34 | word: string,
35 | pluralCase: (word: string) => string = pluralize,
36 | ) {
37 | this.plain = word;
38 | this.camelCase = camelCase(word);
39 | this.pascalCase = pascalCase(word);
40 | this.pluralCase = pluralCase(word);
41 | this.pascalPluralCase = pascalCase(pluralCase(word));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/checks/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Different tests that can be performed to check if sequelize definitions are in sync with the postgres db.
3 | */
4 |
5 | // local
6 | export { default as checkAllowNullConstraint } from './allowNullConstraint';
7 | export { default as checkAssociationConfig } from './associationConfig';
8 | export { default as checkAssociationsSync } from './associations';
9 | export { default as checkBelongsToAssociation } from './belongsToAssociation';
10 | export { default as checkColumnDefinition } from './columnDefinition';
11 | export { default as checkColumnDefinitions } from './columnDefinitions';
12 | export { default as checkColumnType } from './columnType';
13 | export { default as checkDefaultValue } from './defaultValue';
14 | export { default as checkEnumDefinition } from './enumDefinition';
15 | export { default as checkExtraneousTables } from './extraneousTables';
16 | export { default as checkJoinBelongsTo } from './joinBelongsTo';
17 | export { default as checkModel } from './model';
18 | export { default as checkIndexes } from './indexes';
19 | export { default as checkPrimaryKeyDefinition } from './primaryKeyDefinition';
20 | export { default as checkUniqueConstraint } from './uniqueConstraint';
21 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "backoff",
4 | "bytea",
5 | "camelcase",
6 | "conname",
7 | "connamespace",
8 | "conrelid",
9 | "constraintdef",
10 | "esnext",
11 | "executables",
12 | "fkey",
13 | "get",
14 | "jsonb",
15 | "koalaman",
16 | "middlewares",
17 | "mimetype",
18 | "mixins",
19 | "new",
20 | "new parens",
21 | "nobuild",
22 | "npmjs",
23 | "npmrc",
24 | "nspname",
25 | "optionalize",
26 | "parens",
27 | "pathlib",
28 | "pg",
29 | "postgres",
30 | "postgresql",
31 | "pyyaml",
32 | "quotemark",
33 | "requirize",
34 | "setuptools",
35 | "shellcheck",
36 | "symlinks",
37 | "types",
38 | "umzug",
39 | "unnested",
40 | "upsert",
41 | "uuidv"
42 | ],
43 | "[handlebars]": { "editor.formatOnSave": false },
44 | "eslint.validate": [
45 | "javascript",
46 | "javascriptreact",
47 | "typescript",
48 | "typescriptreact"
49 | ],
50 | "editor.codeActionsOnSave": {
51 | "source.fixAll": true
52 | },
53 | "files.exclude": {
54 | "**/node_modules": true
55 | },
56 | "editor.formatOnSave": true,
57 | "typescript.tsdk": "node_modules/typescript/lib"
58 | }
59 |
--------------------------------------------------------------------------------
/src/utils/createUmzugLogger.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { LOG_SKIPS } from '@wildebeest/constants';
3 | import Logger from '@wildebeest/Logger';
4 |
5 | /**
6 | * Only log every 10th migration in test mode
7 | */
8 | export const BASE_100_MIGRATIONS_REGEX = /== ([0-9][0-9]00).+?\(.+?s\)/;
9 |
10 | /**
11 | * Create a logging function to use with umzug
12 | *
13 | * @param logger - The logger to use
14 | * @returns A logging function that can be used to initialize umzug
15 | */
16 | export default function createUmzugLogger(
17 | logger: Logger,
18 | ): (info: string) => void {
19 | return (info: string): void => {
20 | // The logging function to use
21 | const func =
22 | info.includes(': migrated') || info.includes(': reverted')
23 | ? 'info'
24 | : 'debug';
25 |
26 | // Whether to skip
27 | const skip = LOG_SKIPS.filter((tx) => info.includes(tx)).length > 0;
28 | const isBase100 = BASE_100_MIGRATIONS_REGEX.test(info);
29 |
30 | // Determine logging contents
31 | const txt = !isBase100
32 | ? info.replace('\n', '')
33 | : ` ${(BASE_100_MIGRATIONS_REGEX.exec(info) || [])[1]}`;
34 |
35 | // Log if not skipping
36 | if (!skip) {
37 | logger[func](txt);
38 | }
39 | };
40 | }
41 |
--------------------------------------------------------------------------------
/src/utils/listEnumAttributes.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { QueryTypes } from 'sequelize';
3 |
4 | // global
5 | import WildebeestDb from '@wildebeest/classes/WildebeestDb';
6 | import { MigrationTransactionOptions, ModelMap } from '@wildebeest/types';
7 |
8 | /**
9 | * Unnested Value
10 | */
11 | export type UnnestedAttr = {
12 | /** The name of the attribute */
13 | unnest: string;
14 | };
15 |
16 | /**
17 | * List indexes related to a table
18 | *
19 | * @param db - The database to list enum values from
20 | * @param name - The name of the enum to list the values from
21 | * @param rawTransactionOptions - The transaction options
22 | * @returns The enum attributes
23 | */
24 | export default async function listEnumAttributes(
25 | db: WildebeestDb,
26 | name: string,
27 | transactionOptions?: MigrationTransactionOptions,
28 | ): Promise {
29 | // Raw query interface
30 | const { queryInterface } = db;
31 |
32 | // Determine the indexes
33 | const values: UnnestedAttr[] = await queryInterface.sequelize.query(
34 | `SELECT unnest(enum_range(NULL::"${name}"))`,
35 | { ...transactionOptions, type: QueryTypes.SELECT },
36 | );
37 | return values.map(({ unnest }) => unnest);
38 | }
39 |
--------------------------------------------------------------------------------
/src/utils/getColumnDefault.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { QueryTypes } from 'sequelize';
3 |
4 | // global
5 | import WildebeestDb from '@wildebeest/classes/WildebeestDb';
6 | import { MigrationTransactionOptions, ModelMap } from '@wildebeest/types';
7 |
8 | /**
9 | * Get the default value that the column takes on
10 | *
11 | * @param db - The db to check
12 | * @param tableName - The name of the table to check
13 | * @param columnName - The name of the column
14 | * @returns The default value
15 | */
16 | export default async function getColumnDefault(
17 | db: WildebeestDb,
18 | tableName: string,
19 | columnName: string,
20 | transactionOptions?: MigrationTransactionOptions,
21 | ): Promise {
22 | // Get the default for the column
23 | const [
24 | { column_default: columnDefault },
25 | ] = await db.queryInterface.sequelize.query(
26 | `
27 | SELECT column_name, column_default
28 | FROM information_schema.columns
29 | WHERE (table_schema, table_name, column_name) = ('public', '${tableName}', '${columnName}')
30 | ORDER BY ordinal_position;
31 | `,
32 | {
33 | ...transactionOptions,
34 | type: QueryTypes.SELECT,
35 | },
36 | );
37 |
38 | return columnDefault;
39 | }
40 |
--------------------------------------------------------------------------------
/src/migrationTypes/uuidNonNull.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { MigrationDefinition, ModelMap } from '@wildebeest/types';
3 |
4 | // local
5 | import changeColumn from './changeColumn';
6 |
7 | /**
8 | * Options for making a UUID column non null
9 | */
10 | export type UUIDNonNullOptions = {
11 | /** The name of the table to modify */
12 | tableName: string;
13 | /** The name of the uuid column */
14 | columnName: string;
15 | /** When true, drop all null values */
16 | destroy?: boolean;
17 | };
18 |
19 | /**
20 | * Make a uuid column non null
21 | *
22 | * @param options - Options for making the uuid column non-null
23 | * @returns The uuid non null migrator
24 | */
25 | export default function uuidNonNull(
26 | options: UUIDNonNullOptions,
27 | ): MigrationDefinition {
28 | const { tableName, columnName, destroy = false } = options;
29 | return changeColumn({
30 | tableName,
31 | columnName,
32 | getOldColumn: ({ DataTypes }) => ({
33 | allowNull: true,
34 | defaultValue: DataTypes.UUIDV4,
35 | type: DataTypes.UUID,
36 | }),
37 | getNewColumn: ({ DataTypes }) => ({
38 | allowNull: false,
39 | defaultValue: DataTypes.UUIDV4,
40 | type: DataTypes.UUID,
41 | }),
42 | destroy,
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/generators/migrationTypes/alterEnum/index.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import ModelPath from '@generators/prompts/modelPath';
3 |
4 | // migrationTypes
5 | import { listAllAttributeConfigs } from '../tableColumnMigration/lib';
6 |
7 | /**
8 | * A higher order generator that creates a Alter enum migrator.
9 | *
10 | * Alter an existing enum
11 | */
12 | export default {
13 | parentType: 'tableMigration',
14 | prompts: {
15 | enumName: (repo) => ({
16 | message: 'Which enum are you modifying?',
17 | source: ({ modelPath }) =>
18 | listAllAttributeConfigs(repo, modelPath)
19 | .filter(
20 | ({ config }) => config && config.content.includes('DataTypes.ENUM'),
21 | )
22 | .map(
23 | ({ columnName }) =>
24 | `enum_${pluralCase(
25 | ModelPath.getModelName(repo, modelPath),
26 | )}_${columnName}`,
27 | ),
28 | type: 'autocomplete',
29 | postProcess: ({ enumName }) => {
30 | const split = enumName.split('_');
31 | // TODO
32 | const columnName = split.pop();
33 | const tableName = split.slice(1, split.length).join('_');
34 | return {
35 | columnName,
36 | tableName,
37 | };
38 | },
39 | }),
40 | },
41 | };
42 |
--------------------------------------------------------------------------------
/src/utils/writeSchema.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { execSync } from 'child_process';
3 | import { copyFileSync } from 'fs';
4 |
5 | // global
6 | import Wildebeest from '@wildebeest/classes/Wildebeest';
7 | import { ModelMap } from '@wildebeest/types';
8 |
9 | /**
10 | * Write the current database schema to a file
11 | *
12 | * Only will work when RUN apk add --no-cache postgresql-client=10.5-r0 is not commented out in app.Dockerfile
13 | *
14 | * @param wildebeest - The wildebeest db migrator config
15 | * @param name - The name of the schema file
16 | * @returns The schema written
17 | */
18 | export default async function writeSchema(
19 | wildebeest: Wildebeest,
20 | name: string,
21 | ): Promise {
22 | // Hold the output of the sql
23 | const writePath = wildebeest.getSchemaFile(name);
24 | await Promise.resolve(
25 | execSync(`pg_dump -Fc ${wildebeest.db.databaseUri} > ${writePath}`),
26 | );
27 |
28 | // Copy to src so that it can be synced
29 | // TODO custom to transcend remove this eventually
30 | if (writePath.includes('/build/backend/src/')) {
31 | const srcPath = writePath.replace('/build/backend/src/', '/src/');
32 | copyFileSync(writePath, srcPath);
33 | }
34 |
35 | // Log success
36 | wildebeest.logger.warn(`\nWrote schema to dump: "${name}"\n`);
37 | }
38 |
--------------------------------------------------------------------------------
/src/models/migration/hooks.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { ModelMap } from '@wildebeest/types';
3 | import createHooks from '@wildebeest/utils/createHooks';
4 |
5 | // local
6 | import type Migration from './Migration';
7 |
8 | const getNumber = (name: string): number =>
9 | parseInt(name.match(/^\d+/) as any, 10);
10 |
11 | /**
12 | * The default model hooks
13 | */
14 | export default createHooks>({
15 | /**
16 | * Updates the batch number of a new migration
17 | *
18 | * @param migration - The newly created migration
19 | */
20 | afterCreate: async (migration) => {
21 | const { batch } = migration.constructor as typeof Migration;
22 | await migration.update({ batch });
23 | },
24 | /**
25 | * The pre hook before the model is destroyed.
26 | *
27 | * 1) Updates the batch number of any new migration
28 | *
29 | * @param migration - The newly created migration
30 | */
31 | beforeDestroy: async (migration) => {
32 | const index = getNumber(migration.name);
33 |
34 | await (migration.constructor as typeof Migration)
35 | .findAll()
36 | .then((migrations) =>
37 | migrations.filter(({ name }) => getNumber(name) >= index),
38 | )
39 | .then((migrations) =>
40 | Promise.all(migrations.map((mig) => mig.destroy())),
41 | );
42 | },
43 | });
44 |
--------------------------------------------------------------------------------
/src/utils/apply.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { ObjByString } from '@wildebeest/types';
3 |
4 | /**
5 | * The function to apply
6 | */
7 | export type ApplyFunc<
8 | TInput extends ObjByString,
9 | TOutput,
10 | TObjType extends TInput
11 | > = (
12 | value: TInput[keyof TInput],
13 | key: Extract,
14 | fullObj: TObjType,
15 | index: number,
16 | ) => TOutput;
17 |
18 | /**
19 | * Apply a function to each value of an object. Similar to lodash.mapValues but should preserver the typing of the keys.
20 | *
21 | * This allows one to define an object keys in an enum and then the resulting map should keep the same typing
22 | *
23 | * @param obj - The object to apply the function to
24 | * @param applyFunc - The function to apply
25 | * @returns The updated object
26 | */
27 | export default function apply(
28 | obj: TInput,
29 | applyFunc: ApplyFunc,
30 | ): { [key in keyof TInput]: TOutput } {
31 | const result = Object.keys(obj).reduce(
32 | (acc, key, ind) =>
33 | Object.assign(acc, {
34 | [key]: applyFunc(
35 | obj[key],
36 | key as Extract,
37 | obj,
38 | ind,
39 | ),
40 | }),
41 | {},
42 | );
43 | return result as { [key in keyof TInput]: TOutput };
44 | }
45 |
--------------------------------------------------------------------------------
/src/utils/createIndex.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import WildebeestDb from '@wildebeest/classes/WildebeestDb';
3 | import { MigrationTransactionOptions, ModelMap } from '@wildebeest/types';
4 |
5 | /**
6 | * Options for creating an index
7 | * @see http://docs.sequelizejs.com/class/lib/query-interface.js~QueryInterface.html#instance-method-addIndex
8 | */
9 | export type CreateIndexOptions = {
10 | /** The table to create the index in */
11 | tableName: string;
12 | /** The attributes to create the index on */
13 | fields: string[];
14 | /** Whether the index is a unique one */
15 | unique?: boolean;
16 | };
17 |
18 | /**
19 | * Creates a new index, main purpose is to override @types/sequelize
20 | *
21 | * @param db - The db to create the index in
22 | * @param name - Tha name of the index
23 | * @param options - The index options
24 | * @param transactionOptions - The transaction options
25 | */
26 | export default async function createIndex(
27 | db: WildebeestDb,
28 | name: string,
29 | options: CreateIndexOptions,
30 | transactionOptions?: MigrationTransactionOptions,
31 | ): Promise {
32 | const { tableName, fields, ...rest } = options;
33 | await db.queryInterface.addIndex(tableName, fields, {
34 | fields,
35 | name,
36 | ...rest,
37 | ...transactionOptions,
38 | });
39 | }
40 |
--------------------------------------------------------------------------------
/generateMixins/helpers/setConfigDefaults.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { join } from 'path';
3 | import pluralize from 'pluralize';
4 |
5 | // mixins
6 | import { GenerateMixinsConfig } from '../types';
7 |
8 | // local
9 | import defaultDetermineModelToExtend from './determineModelToExtend';
10 |
11 | /**
12 | * Define all properties of config
13 | *
14 | * @param config - The raw config
15 | * @returns The keys of the object preserving type
16 | */
17 | export default function setConfigDefaults({
18 | getCustomMixinAttributes = {},
19 | pluralCase = pluralize,
20 | singularCase = pluralize.singular,
21 | baseFileName = 'Base.ts',
22 | associationFileName = 'definition.ts',
23 | associationDefinitionName = 'associations',
24 | tsConfigPath,
25 | baseModelTemplatePath = join(__dirname, '..', 'base.hbs'),
26 | determineModelToExtend = defaultDetermineModelToExtend,
27 | handlebarsHelpers = {},
28 | ...rest
29 | }: GenerateMixinsConfig): Required {
30 | return {
31 | ...rest,
32 | handlebarsHelpers,
33 | determineModelToExtend,
34 | baseFileName,
35 | getCustomMixinAttributes,
36 | singularCase,
37 | pluralCase,
38 | baseModelTemplatePath,
39 | tsConfigPath: tsConfigPath || join(rest.rootPath, 'tsconfig.json'),
40 | associationFileName,
41 | associationDefinitionName,
42 | };
43 | }
44 |
--------------------------------------------------------------------------------
/src/utils/inferTableReference.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import camelCase from 'lodash/camelCase';
3 |
4 | /**
5 | * The foreign key table reference the constraint should be made with
6 | */
7 | export type TableReference = {
8 | /** The name of the table */
9 | table: string;
10 | /** The field aka columnName in the table to reference */
11 | field: string;
12 | };
13 |
14 | /**
15 | * Infer the default reference fields from a column
16 | *
17 | * @example Example usage of inferTableReference
18 | * // { field: 'id', table: 'organizations' }
19 | * inferTableReference('organizationId');
20 | *
21 | * @param column - The name of the column to add the cascade to
22 | * @param modelNameToTableName - A function that will convert a model name to table name
23 | * @returns The table and field extracted from the column
24 | */
25 | export default function inferTableReference(
26 | column: string,
27 | modelNameToTableName: (word: string) => string,
28 | ): TableReference {
29 | // Split the column by camel case
30 | const splitColumn = column.split(/(?=[A-Z])/);
31 |
32 | // The reference table field is by default the camel case version of the camel case word
33 | const field = camelCase(splitColumn.pop());
34 |
35 | // Determine the name of the reference table
36 | const table = modelNameToTableName(splitColumn.join(''));
37 |
38 | return { table, field };
39 | }
40 |
--------------------------------------------------------------------------------
/src/views/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Handlebars templates used for the running wildebeest migrations.
3 | */
4 |
5 | // external
6 | import { readFileSync } from 'fs';
7 | import Handlebars from 'handlebars';
8 |
9 | // helpers
10 | import type { ObjByString } from '@wildebeest/types';
11 | import pascalCase from '@wildebeest/utils/pascalCase';
12 | import { join } from 'path';
13 |
14 | // Set the helpers
15 | Handlebars.registerHelper('pascalCase', pascalCase);
16 | Handlebars.registerHelper('pad', (word: string) => '/'.repeat(word.length));
17 | Handlebars.registerHelper('ifNotEqual', function ifNotEqual(
18 | this: any,
19 | arg1: string,
20 | arg2: string,
21 | options: Handlebars.HelperOptions,
22 | ) {
23 | return arg1 !== arg2 ? options.fn(this) : options.inverse(this);
24 | });
25 |
26 | const TEMPLATES = {
27 | error: readFileSync(join(__dirname, 'error.hbs'), 'utf-8'),
28 | index: readFileSync(join(__dirname, 'index.hbs'), 'utf-8'),
29 | success: readFileSync(join(__dirname, 'success.hbs'), 'utf-8'),
30 | };
31 |
32 | /**
33 | * Render some HTML
34 | *
35 | * @param name - The name of the template to use
36 | * @param params - The handlebars compilation params
37 | * @returns The HTML template
38 | */
39 | export default function render(
40 | name: keyof typeof TEMPLATES,
41 | params: ObjByString,
42 | ): string {
43 | return Handlebars.compile(TEMPLATES[name])(params);
44 | }
45 |
--------------------------------------------------------------------------------
/generateMixins/helpers/determineModelToExtend.ts:
--------------------------------------------------------------------------------
1 | // mixins
2 | import {
3 | AssociationsDefinition,
4 | BaseFileInput,
5 | GenerateMixinsConfig,
6 | } from '../types';
7 |
8 | // local
9 | import pascalCase from './pascalCase';
10 |
11 | /**
12 | * Determine the model that the `Base` should extend
13 | *
14 | * @param directory - The directory to look for the tsconfig in
15 | * @param tsConfigPath - The path to the tsconfig if not in the expected tsconfig.json location
16 | * @returns The tsconfig or throws an error
17 | */
18 | export default function determineModelToExtend(
19 | associationsDefinition: AssociationsDefinition,
20 | config: Required,
21 | ): Omit {
22 | // Get te file path with
23 | const split = associationsDefinition.filePath.split('/');
24 | if (split[split.length - 1] === config.associationFileName) {
25 | split.pop();
26 | }
27 |
28 | const [modelBaseFolderRaw, container] = split.slice(-2);
29 | const modelBaseFolder = ['src', 'models'].includes(modelBaseFolderRaw)
30 | ? 'db'
31 | : modelBaseFolderRaw;
32 | const ModelBaseName =
33 | modelBaseFolder === 'db'
34 | ? 'Model'
35 | : `${pascalCase(config.singularCase(modelBaseFolder))}Model`;
36 |
37 | return {
38 | container,
39 | ModelBaseName,
40 | modelBaseFolder,
41 | modelDefinitionsPath: config.modelDefinitionsPath,
42 | };
43 | }
44 |
--------------------------------------------------------------------------------
/src/utils/getAssociationsByModelName.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BelongsToAssociation,
3 | HasManyAssociation,
4 | HasOneAssociation,
5 | } from '@wildebeest/types';
6 |
7 | // local
8 | import getKeys from './getKeys';
9 |
10 | /**
11 | * No belongs to many
12 | */
13 | export type AssociationWithoutBelongsToMany =
14 | | BelongsToAssociation
15 | | HasManyAssociation
16 | | HasOneAssociation;
17 |
18 | /**
19 | * Get the association definitions for a given model name
20 | *
21 | * @param modelName - The name of the model to check for
22 | * @param associations - The association lookup to check for model name in
23 | * @returns The keys of the object preserving type
24 | */
25 | export default function getAssociationsByModelName(
26 | modelName: TModelNames,
27 | associations: {
28 | [k in string]: AssociationWithoutBelongsToMany;
29 | },
30 | ): AssociationWithoutBelongsToMany[] {
31 | return getKeys(associations)
32 | .filter((associationName) => {
33 | const association = associations[associationName];
34 | const associationModelName =
35 | typeof association === 'object' && association.modelName
36 | ? association.modelName
37 | : associationName;
38 | return associationModelName === modelName;
39 | })
40 | .map((associationName) => associations[associationName]);
41 | }
42 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # See http://pre-commit.com for more information
2 | # See http://pre-commit.com/hooks.html for more hooks
3 | repos:
4 | - repo: https://github.com/pre-commit/pre-commit-hooks
5 | sha: v1.3.0
6 | hooks:
7 | - id: check-merge-conflict
8 | - id: check-symlinks
9 | - id: check-case-conflict
10 | - id: check-executables-have-shebangs
11 | - id: detect-private-key
12 | exclude: (ssl/)
13 | - id: detect-aws-credentials
14 | args: [--allow-missing-credentials]
15 | - id: check-yaml
16 | files: (yaml$|yml$)
17 | - repo: local
18 | hooks:
19 | - id: shellcheck-lint
20 | name: Shellcheck
21 | language: docker_image
22 | # https://github.com/koalaman/shellcheck
23 | entry: koalaman/shellcheck:v0.5.0
24 | types: [shell]
25 | - id: eslint
26 | name: ESLint
27 | language: system
28 | entry: ./node_modules/.bin/eslint
29 | args: [--fix]
30 | types: [file]
31 | files: \.(js|jsx|ts|tsx)$
32 | exclude: (generators/) # TODO
33 | - id: tslint
34 | name: TSLint
35 | language: system
36 | entry: ./node_modules/.bin/tslint
37 | args: [--fix]
38 | types: [file]
39 | files: \.(js|jsx|ts|tsx)$
40 | exclude: (generators/) # TODO
41 | - id: prettier
42 | name: Prettier
43 | language: node
44 | entry: ./node_modules/.bin/prettier
45 | args: [--write]
46 |
--------------------------------------------------------------------------------
/src/checks/matchingBelongsTo.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import Wildebeest from '@wildebeest/classes/Wildebeest';
3 | import { ModelMap, StringKeys, SyncError } from '@wildebeest/types';
4 | import getAssociationsByModelName from '@wildebeest/utils/getAssociationsByModelName';
5 |
6 | /**
7 | * For `hasOne` and `hasMany` relations, check that they have an opposing `belongsTo` association configured.
8 | *
9 | * @param wildebeest - The wildebeest config
10 | * @param modelName - The name of the model that has the `hasOne` or `hasMany` relations
11 | * @param associationTable - The name of the table being associated
12 | * @returns Any errors related to mismatching belongs to configurations
13 | */
14 | export default function checkMatchingBelongsTo(
15 | wildebeest: Wildebeest,
16 | modelName: StringKeys,
17 | associationTable: StringKeys,
18 | ): SyncError[] {
19 | // Keep track of errors
20 | const errors: SyncError[] = [];
21 |
22 | // Get the associations of the association
23 | const { associations = {} } = wildebeest.getModelDefinition(associationTable);
24 | const { belongsTo = {} } = associations;
25 |
26 | // Ensure `modelName` is found in one of the associations
27 | if (getAssociationsByModelName(modelName, belongsTo).length === 0) {
28 | errors.push({
29 | message: `Missing belongsTo opposite on "${associationTable}" to ${modelName}`,
30 | tableName: wildebeest.pluralCase(modelName),
31 | });
32 | }
33 |
34 | return errors;
35 | }
36 |
--------------------------------------------------------------------------------
/src/checks/allowNullConstraint.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { ModelAttributeColumnOptions } from 'sequelize';
3 |
4 | // global
5 | import { ModelMap, SyncError } from '@wildebeest/types';
6 | import columnAllowsNull from '@wildebeest/utils/columnAllowsNull';
7 |
8 | // classes
9 | import WildebeestDb from '@wildebeest/classes/WildebeestDb';
10 |
11 | /**
12 | * Check that the allowNull constraint is setup properly on the column
13 | *
14 | * @param name - The name of the attribute
15 | * @param definition - The attribute definition
16 | * @returns Any errors with the allow null constraint
17 | */
18 | export default async function checkAllowNullConstraint<
19 | TModels extends ModelMap
20 | >(
21 | db: WildebeestDb,
22 | tableName: string,
23 | name: string,
24 | definition: ModelAttributeColumnOptions,
25 | ): Promise {
26 | // The sync errors found
27 | const errors: SyncError[] = [];
28 |
29 | // Check if expected to be unique
30 | const allowsNull = definition.allowNull === true;
31 |
32 | // Check if the column allows null values exists
33 | const dbAllowsNull = await columnAllowsNull(db, tableName, name);
34 |
35 | // Ensure both align
36 | if (allowsNull !== dbAllowsNull) {
37 | errors.push({
38 | message: dbAllowsNull
39 | ? `Missing nonNull constraint for column "${name}" in table "${tableName}"`
40 | : `Extra nonNull constraint for column "${name}" in table "${tableName}"`,
41 | tableName,
42 | });
43 | }
44 |
45 | return errors;
46 | }
47 |
--------------------------------------------------------------------------------
/src/utils/listConstraintNames.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { QueryTypes } from 'sequelize';
3 |
4 | // global
5 | import WildebeestDb from '@wildebeest/classes/WildebeestDb';
6 | import { MigrationTransactionOptions, ModelMap } from '@wildebeest/types';
7 |
8 | /**
9 | * A db constraint
10 | */
11 | export type PgConstraint = {
12 | /** The name of the constraint */
13 | conname: string;
14 | };
15 |
16 | /**
17 | * List constraints related to a table
18 | *
19 | * @param db - The db instance to check
20 | * @param tableName - The table to list constraints for
21 | * @returns The names of the constraints
22 | */
23 | export default async function listConstraintNames(
24 | db: WildebeestDb,
25 | tableName: string,
26 | transactionOptions?: MigrationTransactionOptions,
27 | ): Promise {
28 | const { queryInterface } = db;
29 | // Determine the constraints
30 | const constraints: PgConstraint[] = await queryInterface.sequelize.query(
31 | `SELECT con.*
32 | FROM pg_catalog.pg_constraint con
33 | INNER JOIN pg_catalog.pg_class rel
34 | ON rel.oid = con.conrelid
35 | INNER JOIN pg_catalog.pg_namespace nsp
36 | ON nsp.oid = connamespace
37 | WHERE nsp.nspname = 'public'
38 | AND rel.relname = '${tableName}'`,
39 | {
40 | ...transactionOptions,
41 | type: QueryTypes.SELECT,
42 | },
43 | );
44 | return constraints.map(({ conname }) => conname);
45 | }
46 |
--------------------------------------------------------------------------------
/src/utils/renameS3File.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { S3 } from 'aws-sdk';
3 |
4 | // global
5 | import Wildebeest from '@wildebeest/classes/Wildebeest';
6 | import { ModelMap } from '@wildebeest/types';
7 |
8 | /**
9 | * Rename an s3 file
10 | *
11 | * @param Bucket - The name of the s3 bucket
12 | * @param oldKey - The old key
13 | * @param newKey - The new key
14 | * @param mimetype - The mimetype of the file
15 | * @returns The rename promise
16 | */
17 | export default async function renameS3File(
18 | wildebeest: Wildebeest,
19 | s3: S3,
20 | Bucket: string,
21 | oldKey: string,
22 | newKey: string,
23 | mimetype?: string,
24 | isDryRun = process.env.DRY_RUN === 'true',
25 | ): Promise {
26 | // The copy parameters
27 | const copyParams = {
28 | ...(mimetype ? { ContentType: mimetype } : {}),
29 | Bucket,
30 | MetadataDirective: 'REPLACE',
31 | CopySource: `/${Bucket}/${oldKey}`,
32 | Key: newKey,
33 | };
34 |
35 | // Copy the object
36 | if (isDryRun) {
37 | wildebeest.logger.info(`Copied: "${oldKey}" to "${newKey}"`);
38 | } else {
39 | await s3.copyObject(copyParams).promise();
40 | }
41 |
42 | // The delete parameters
43 | const deleteParams = {
44 | Bucket,
45 | Delete: { Objects: [{ Key: oldKey }] },
46 | };
47 |
48 | // Delete the old object
49 | if (isDryRun) {
50 | wildebeest.logger.info(`Deleted: "${oldKey}"`);
51 | } else {
52 | await s3.deleteObjects(deleteParams).promise();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/migrationTypes/custom.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import Wildebeest from '@wildebeest/classes/Wildebeest';
3 | import {
4 | MigrationDefinition,
5 | MigrationTransactionOptions,
6 | ModelMap,
7 | } from '@wildebeest/types';
8 |
9 | /**
10 | * A migration that should run on the db
11 | */
12 | export type TransactionMigrationDefinition = {
13 | /** The up migration (there should be no loss of data) */
14 | up: (
15 | transactionOptions: MigrationTransactionOptions,
16 | wildebeest: Wildebeest,
17 | ) => Promise; // eslint-disable-line @typescript-eslint/no-explicit-any
18 | /** The down migration to reverse the up migration, with potential loss of data */
19 | down: (
20 | transactionOptions: MigrationTransactionOptions,
21 | wildebeest: Wildebeest,
22 | ) => Promise; // eslint-disable-line @typescript-eslint/no-explicit-any
23 | };
24 |
25 | /**
26 | * A helper to wrap a custom migration in a transaction
27 | *
28 | * @param options - The custom options
29 | * @returns The custom migrator
30 | */
31 | export default function custom(
32 | options: TransactionMigrationDefinition,
33 | ): MigrationDefinition {
34 | const { up, down } = options;
35 | return {
36 | up: async (wildebeest, withTransaction) =>
37 | withTransaction((transactionOptions) =>
38 | up(transactionOptions, wildebeest),
39 | ),
40 | down: async (wildebeest, withTransaction) =>
41 | withTransaction((transactionOptions) =>
42 | down(transactionOptions, wildebeest),
43 | ),
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/src/migrationTypes/renameIndex.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import {
3 | MigrationDefinition,
4 | MigrationTransactionOptions,
5 | ModelMap,
6 | } from '@wildebeest/types';
7 |
8 | /**
9 | * The options for renaming a table index
10 | */
11 | export type RenameIndexOptions = {
12 | /** The old name of the index */
13 | oldName: string;
14 | /** The new name of the index */
15 | newName: string;
16 | };
17 |
18 | /**
19 | * Rename an index
20 | *
21 | * @param options - The rename options
22 | * @param rawTransactionOptions - The existing transaction
23 | * @returns The rename table promise
24 | */
25 | export async function renameTableIndex(
26 | { oldName, newName }: RenameIndexOptions,
27 | { queryT }: MigrationTransactionOptions,
28 | ): Promise {
29 | await queryT.raw(`ALTER INDEX "${oldName}" RENAME TO "${newName}"`);
30 | }
31 |
32 | /**
33 | * Rename an index
34 | *
35 | * @param options - Rename index options
36 | * @returns The rename index migrator
37 | */
38 | export default function renameIndex(
39 | options: RenameIndexOptions,
40 | ): MigrationDefinition {
41 | return {
42 | up: async (_, withTransaction) =>
43 | withTransaction((transactionOptions) =>
44 | renameTableIndex(options, transactionOptions),
45 | ),
46 | down: async (_, withTransaction) =>
47 | withTransaction((transactionOptions) =>
48 | renameTableIndex(
49 | {
50 | newName: options.oldName,
51 | oldName: options.newName,
52 | },
53 | transactionOptions,
54 | ),
55 | ),
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/generateMixins/helpers/createBase.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { writeFileSync } from 'fs';
3 | import { join } from 'path';
4 |
5 | // mixins
6 | import {
7 | AssociationsDefinition,
8 | BaseFileInput,
9 | GenerateMixinsConfig,
10 | } from '../types';
11 |
12 | // local
13 | import associationDefinitionToSections from './associationDefinitionToSections';
14 | import determineModelToExtend from './determineModelToExtend';
15 |
16 | /**
17 | * Process file
18 | *
19 | * @param associationsDefinition - The associations defined for a db mode
20 | * @param createBaseFile - A function to construct the base model file (usually handlebars compiled template)
21 | * @param config - The config for generating mixins
22 | */
23 | export default function createBase(
24 | associationsDefinition: AssociationsDefinition,
25 | createBaseFile: (input: BaseFileInput) => string,
26 | config: Required,
27 | ): void {
28 | // convert into input sections for the handlebars template
29 | const allAssociations = associationDefinitionToSections(
30 | associationsDefinition,
31 | config,
32 | );
33 |
34 | // Get te file path with
35 | const splitter = associationsDefinition.filePath.split('/');
36 | if (splitter[splitter.length - 1] === config.associationFileName) {
37 | splitter.pop();
38 | }
39 |
40 | const compiled = createBaseFile({
41 | ...(config.determineModelToExtend(associationsDefinition, config) ||
42 | determineModelToExtend(associationsDefinition, config)),
43 | sections: allAssociations,
44 | });
45 |
46 | writeFileSync(
47 | join(splitter.concat([config.baseFileName]).join('/')),
48 | compiled,
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/src/routes.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Routes for running migrations
3 | */
4 |
5 | // external
6 | import express from 'express';
7 |
8 | // local
9 | import Wildebeest from '@wildebeest/classes/Wildebeest';
10 | import { ModelMap } from '@wildebeest/types';
11 | import * as controller from './controller';
12 |
13 | /**
14 | * Construct a router
15 | */
16 | export default (
17 | wildebeest: Wildebeest,
18 | ): express.Router => {
19 | // instantiate a router
20 | const router = express.Router();
21 |
22 | // /////////// //
23 | // MIDDLEWARES //
24 | // /////////// //
25 |
26 | // Save the wildebeest onto res.locals
27 | router.all('*', (_, res, next) => {
28 | res.locals.wildebeest = wildebeest;
29 | next();
30 | });
31 |
32 | // /////////// //
33 | // CONTROLLERS //
34 | // /////////// //
35 |
36 | // Display the seed routes
37 | router.get('/', controller.renderRoutes);
38 |
39 | // Migrate all the way forward
40 | router.get('/current', controller.current);
41 |
42 | // Migrate all the way back
43 | router.get('/empty', controller.empty);
44 |
45 | // Check if the db is in sync with code
46 | router.get('/sync', controller.sync);
47 |
48 | // Run genesis down, then all the way up, down up, down, up
49 | router.get('/test', controller.test);
50 |
51 | // Run up to this migration
52 | router.get('/up/:num', controller.up);
53 |
54 | // Run down to this migration
55 | router.get('/down/:num', controller.down);
56 |
57 | // Write the current schema to file
58 | if (wildebeest.allowSchemaWrites) {
59 | router.get('/write-schema/:name', controller.writeSchemaByName);
60 | }
61 | return router;
62 | };
63 |
--------------------------------------------------------------------------------
/generateMixins/base.hbs:
--------------------------------------------------------------------------------
1 | /* tslint:disable completed-docs */
2 | /**
3 | *
4 | * ## {{ pascalCase container }} Base Model
5 | * The db model definition from which this model definition is inheriting from.
6 | *
7 | * WARNING: This file is 100% generated.
8 | *
9 | * This is the base class definition with all [Sequelize mixins](https://sequelize.readthedocs.io/en/latest/api/associations/) applied.
10 | *
11 | * This class definition is an [abstract](https://www.typescriptlang.org/docs/handbook/classes.html#abstract-classes) class and may require a certain API to be properly implemented.
12 | */
13 |
14 | // wildebeest
15 | import * as w from '@transcend-io/wildebeest';
16 |
17 | // db
18 | import type * as M from '{{ modelDefinitionsPath }}';
19 | {{#ifEqual modelBaseFolder "db"}}
20 | import {{ ModelBaseName }} from '@bk/{{ modelBaseFolder }}/{{ ModelBaseName }}';
21 | {{/ifEqual}}
22 | {{#ifNotEqual modelBaseFolder "db"}}
23 |
24 | // {{ modelBaseFolder }}
25 | import {{ ModelBaseName }} from '@bk/{{ modelBaseFolder }}/{{ ModelBaseName }}';
26 | {{/ifNotEqual}}
27 |
28 | /**
29 | * This is the base class definition with all mixins applied from associations on the {{ pascalCase container }}
30 | */
31 | export default abstract class {{ pascalCase container }}Base extends {{ ModelBaseName }} {
32 | {{#each sections}}
33 |
34 | // {{ pad associationType }} //
35 | // {{ associationType }} //
36 | // {{ pad associationType }} //
37 | {{#each definitions}}
38 |
39 | /** Association with `{{ modelName }}`{{#ifNotEqual associationName modelName}} as `{{ associationName }}`{{/ifNotEqual}} */
40 | {{#each attributes}}
41 | {{{ attribute }}}
42 | {{/each}}
43 | {{/each}}
44 | {{/each}}
45 | };
46 |
--------------------------------------------------------------------------------
/generators/generators/migration/index.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { paramCase } from 'change-case';
3 |
4 | // global
5 | import migrationTypes from '@generators/migrationTypes';
6 |
7 | const DEFAULT_TEMPLATE = (generatorName): string => `{{> dbMigrationHeader }}
8 | // migrationTypes
9 | import ${generatorName} from '@bk/migrations/migrationTypes/${generatorName}';${'\n'}
10 | ${`${'module.'}${'exports'}`} = ${generatorName}({
11 | TODO
12 | });
13 | `;
14 |
15 | /**
16 | * Create a new migration type
17 | *
18 | * TODO wildebeest
19 | */
20 | export default {
21 | description: 'Create a new migration type',
22 | location: 'migrations',
23 | prompts: {
24 | generatorType: {
25 | source: () => ['', ...Object.keys(migrationTypes)],
26 | message: 'What type of migration?',
27 | type: 'autocomplete',
28 | },
29 | templateContents: {
30 | default: ({ container }) => DEFAULT_TEMPLATE(container),
31 | message: 'What is the migration template?',
32 | extension: 'hbs',
33 | type: 'editor',
34 | },
35 | nameExt: {
36 | message: ({ container }) =>
37 | `What should be appended to "${paramCase(
38 | container,
39 | )}" to create the name of the migration?`,
40 | type: 'input',
41 | required: true,
42 | },
43 | defaultComment: {
44 | message: 'What should be the default comment for the migration?',
45 | type: 'input',
46 | required: true,
47 | },
48 | },
49 | requiredFiles: [
50 | {
51 | generator: 'handlebars-template',
52 | config: () => ({ name: 'template', extension: 'hbs' }),
53 | },
54 | ],
55 | skipActions: true,
56 | type: 'generatorContainer',
57 | };
58 |
--------------------------------------------------------------------------------
/src/checks/extraneousTables.ts:
--------------------------------------------------------------------------------
1 | // external moduels
2 | import difference from 'lodash/difference';
3 | import { QueryTypes } from 'sequelize';
4 |
5 | // global
6 | import { ModelMap, SyncError } from '@wildebeest/types';
7 |
8 | // classes
9 | import WildebeestDb from '@wildebeest/classes/WildebeestDb';
10 |
11 | /**
12 | * When querying a table
13 | */
14 | export type TableDefinition = {
15 | /** The postgres schema the table belongs to */
16 | schemaname: string;
17 | /** The name of the table */
18 | tablename: string;
19 | };
20 |
21 | /**
22 | * Ensure the default value of a sequelize definition matches the default value in postgres
23 | *
24 | * @param wildebeest - The wildebeest db to operate on
25 | * @param tableNames - The tables names that should exist
26 | * @returns Any errors related to extra tables.
27 | */
28 | export default async function checkExtraneousTables(
29 | db: WildebeestDb,
30 | tableNames: string[],
31 | ): Promise {
32 | // Keep track of errors
33 | const errors: SyncError[] = [];
34 |
35 | // Check for existing tables tables
36 | const tables: TableDefinition[] = await db.query(
37 | 'SELECT schemaname, tablename FROM pg_catalog.pg_tables where "schemaname"=\'public\';',
38 | { type: QueryTypes.SELECT },
39 | );
40 |
41 | // Determine if there are any extra
42 | const extraTables = difference(
43 | tables.map(({ tablename }) => tablename),
44 | tableNames,
45 | );
46 | if (extraTables.length > 0) {
47 | extraTables.map((tableName) =>
48 | errors.push({
49 | message: `\nExtra table definitions: "${extraTables.join('", "')}"`,
50 | tableName,
51 | }),
52 | );
53 | }
54 |
55 | return errors;
56 | }
57 |
--------------------------------------------------------------------------------
/src/enums.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import mkEnum from '@wildebeest/utils/mkEnum';
3 |
4 | /**
5 | * A type of association
6 | */
7 | export enum AssociationType {
8 | /** Belongs to add id to col */
9 | BelongsTo = 'belongsTo',
10 | /** Join */
11 | BelongsToMany = 'belongsToMany',
12 | /** Has many */
13 | HasMany = 'hasMany',
14 | /** Has one */
15 | HasOne = 'hasOne',
16 | }
17 |
18 | /**
19 | * The supported postgres index types
20 | */
21 | export enum IndexType {
22 | /** An index on a primary key */
23 | PrimaryKey = 'primary key',
24 | /** A column or set of columns that are unique */
25 | Unique = 'unique',
26 | /** (default) A regular database index */
27 | Index = 'index',
28 | }
29 |
30 | /**
31 | * The supported postgres index methods
32 | */
33 | export enum IndexMethod {
34 | /** A binary tree index */
35 | BTree = 'BTREE',
36 | /** A hash index */
37 | Hash = 'HASH',
38 | /** Gist index */
39 | Gist = 'GIST',
40 | /** Gin index */
41 | Gin = 'GIN',
42 | /** Sp gist index */
43 | SPGist = 'SPGIST',
44 | /** Brin index */
45 | Brin = 'BRIN',
46 | }
47 |
48 | /**
49 | * The names of the db models can be overwritten
50 | */
51 | export const DefaultTableNames = mkEnum({
52 | migrationLock: 'migrationLocks',
53 | migration: 'migrations',
54 | });
55 |
56 | /**
57 | * Possible options on association model delete
58 | */
59 | export const OnDelete = mkEnum({
60 | /** Cascade the deletion to this table */
61 | Cascade: 'CASCADE',
62 | /** Set the column to null */
63 | SetNull: 'SET NULL',
64 | /** Do nothing (often not used) */
65 | NoAction: 'NO ACTION',
66 | });
67 |
68 | /**
69 | * Overload with type of on delete
70 | */
71 | export type OnDelete = typeof OnDelete[keyof typeof OnDelete];
72 |
--------------------------------------------------------------------------------
/generators/index.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import difference from 'lodash/difference';
3 | import kebabCase from 'lodash/kebabCase';
4 |
5 | // global
6 | import logger from '@generators/logger';
7 | import convertToPlop from '@generators/utils/convertToPlop';
8 |
9 | /**
10 | * Create a db migration
11 | */
12 | export default {
13 | description: 'Run a db migration',
14 | getGenerators: (
15 | { actions, cwdFilter, description, generatorName, location, prompts },
16 | { count, cwdLocation, name, options },
17 | repo,
18 | ) => {
19 | // The refactor generator
20 | const generators = [
21 | {
22 | generatorName,
23 | name,
24 | count,
25 | init: convertToPlop(
26 | {
27 | actions,
28 | cwdFilter,
29 | cwdLocation,
30 | description,
31 | location,
32 | options,
33 | prompts,
34 | },
35 | repo,
36 | ),
37 | },
38 | ];
39 |
40 | return generators;
41 | },
42 | // Check that all migrationTypes have a corresponding generator
43 | postSetup: (generators, repo) => {
44 | // The migrationTypes that have been defined
45 | const migrationTypes = repo
46 | .listEntryFiles('migrations/migrationTypes')
47 | .map((fil) => kebabCase(fil.split('.')[0]))
48 | .filter((fil) => !['index', 'skip'].includes(fil));
49 |
50 | // The existing generators
51 | const migrationTypesGenerators = generators.map(({ name }) => name);
52 |
53 | // Missing generators
54 | const missing = difference(migrationTypes, migrationTypesGenerators);
55 | if (missing.length) {
56 | logger.error(`Missing migration generators for: "${missing.join(', ')}"`);
57 | }
58 | },
59 | };
60 |
--------------------------------------------------------------------------------
/src/migrationTypes/addIndex.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { MigrationDefinition, ModelMap } from '@wildebeest/types';
3 | import createIndex from '@wildebeest/utils/createIndex';
4 |
5 | /**
6 | * Options for adding a new index
7 | */
8 | export type AddIndexOptions = {
9 | /** The name of the table to add the constraint to */
10 | tableName: string;
11 | /** The name of the columns to make unique across */
12 | fields: string[];
13 | /** Override the constraint name (leave null for default) */
14 | constraintName?: string;
15 | };
16 |
17 | /**
18 | * Create a regular index on a column or set of columns in a table
19 | *
20 | * @param options - Options for making a unique constraint across multiple columns
21 | * @returns The add index migrator
22 | */
23 | export default function addIndex(
24 | options: AddIndexOptions,
25 | ): MigrationDefinition {
26 | const { tableName, fields, constraintName } = options;
27 | return {
28 | // Add the unique index
29 | up: async ({ db, namingConventions }, withTransaction) =>
30 | withTransaction((transactionOptions) =>
31 | createIndex(
32 | db,
33 | constraintName ||
34 | namingConventions.fieldsConstraint(tableName, fields),
35 | {
36 | tableName,
37 | fields,
38 | },
39 | transactionOptions,
40 | ),
41 | ),
42 | // Remove the index
43 | down: async ({ db, namingConventions }, withTransaction) =>
44 | withTransaction((transactionOptions) =>
45 | db.queryInterface.removeIndex(
46 | tableName,
47 | constraintName ||
48 | namingConventions.fieldsConstraint(tableName, fields),
49 | transactionOptions,
50 | ),
51 | ),
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/src/migrationTypes/renameEnum.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import {
3 | MigrationDefinition,
4 | MigrationTransactionOptions,
5 | ModelMap,
6 | } from '@wildebeest/types';
7 |
8 | /**
9 | * The options for changing the name of an enum
10 | */
11 | export type RenameEnumOptions = {
12 | /** The old name of the enum */
13 | oldName: string;
14 | /** The new name of the enum */
15 | newName: string;
16 | };
17 |
18 | /**
19 | * Change the name of an enum
20 | *
21 | * @param options - The change enum options
22 | * @param rawTransactionOptions - The raw transaction options
23 | * @returns The change name promise
24 | */
25 | export async function changeEnumName(
26 | options: RenameEnumOptions,
27 | transactionOptions: MigrationTransactionOptions,
28 | ): Promise {
29 | // Raw query interface
30 | const { queryT } = transactionOptions;
31 |
32 | // Change the name
33 | await queryT.raw(
34 | `ALTER TYPE "${options.oldName}" RENAME TO "${options.newName}"`,
35 | );
36 | }
37 |
38 | /**
39 | * Rename an enum definition
40 | *
41 | * @param options - The change enum options
42 | * @returns The rename enum migrator
43 | */
44 | export default function renameEnum(
45 | options: RenameEnumOptions,
46 | ): MigrationDefinition {
47 | const { oldName, newName } = options;
48 | return {
49 | up: async (_, withTransaction) =>
50 | withTransaction((transactionOptions) =>
51 | changeEnumName({ oldName, newName }, transactionOptions),
52 | ),
53 | down: async (_, withTransaction) =>
54 | withTransaction((transactionOptions) =>
55 | changeEnumName(
56 | { newName: oldName, oldName: newName },
57 | transactionOptions,
58 | ),
59 | ),
60 | };
61 | }
62 |
--------------------------------------------------------------------------------
/generateMixins/index.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { readFileSync } from 'fs';
3 |
4 | // local
5 | import Handlebars from './Handlebars';
6 | import {
7 | createBase,
8 | extractAssociationDefinitions,
9 | getCompilerOptions,
10 | setConfigDefaults,
11 | } from './helpers';
12 | import { BaseFileInput, GenerateMixinsConfig } from './types';
13 |
14 | /**
15 | * Main runner for mixin generation
16 | *
17 | * @param configPath - The path to the config specified via command line arg
18 | */
19 | export default function generateMixins(
20 | mixinConfig: GenerateMixinsConfig,
21 | ): void {
22 | // Read in the config and set defaults
23 | const config = setConfigDefaults(mixinConfig);
24 |
25 | // Register any custom handlebars helpers
26 | Object.entries(config.handlebarsHelpers).map(([name, func]) =>
27 | Handlebars.registerHelper(name, func),
28 | );
29 |
30 | // Compile the handlebars template
31 | const TEMPLATE = Handlebars.compile(
32 | readFileSync(config.baseModelTemplatePath, 'utf-8'),
33 | );
34 |
35 | // Get the typescript compiler options
36 | const compilerOptions = getCompilerOptions(
37 | config.rootPath,
38 | config.tsConfigPath,
39 | );
40 |
41 | // Extract the association definitions for each model
42 | const associationFiles = extractAssociationDefinitions(
43 | [`${config.rootPath}/${config.src}`],
44 | {
45 | ...compilerOptions,
46 | moduleResolution: undefined,
47 | },
48 | config,
49 | );
50 |
51 | // Process each definition and generate the Base model file
52 | associationFiles.forEach((associationDefinition) =>
53 | createBase(associationDefinition, TEMPLATE, config),
54 | );
55 |
56 | // print out the doc
57 | // writeFileSync('types.json', JSON.stringify(output, undefined, 4));
58 | }
59 |
--------------------------------------------------------------------------------
/src/Logger.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console,class-methods-use-this */
2 | // external
3 | /* tslint:disable no-var-requires */
4 | const chalk = require('chalk'); // eslint-disable-line @typescript-eslint/no-var-requires
5 | /* tslint:enable no-var-requires */
6 |
7 | const applyChalkColor = (
8 | color: 'red' | 'green' | 'blue' | 'bold' | 'yellow',
9 | props: Parameters,
10 | ): Parameters =>
11 | props.map((p) => (typeof p === 'string' ? chalk[color](p) : p)) as Parameters<
12 | typeof console.log
13 | >;
14 |
15 | /**
16 | * The logger can be overwritten with custom logging interfaces, however we require the logger to implement the following
17 | */
18 | export default class Logger {
19 | /**
20 | * Standard console.log
21 | */
22 | public log(...props: Parameters): void {
23 | console.log(...props);
24 | }
25 |
26 | /**
27 | * Standard console.error
28 | */
29 | public error(...props: Parameters): void {
30 | console.error(...applyChalkColor('red', props));
31 | }
32 |
33 | /**
34 | * Standard console.warn
35 | */
36 | public warn(...props: Parameters): void {
37 | console.warn(...applyChalkColor('yellow', props));
38 | }
39 |
40 | /**
41 | * Standard console.info
42 | */
43 | public info(...props: Parameters): void {
44 | console.info(...applyChalkColor('blue', props));
45 | }
46 |
47 | /**
48 | * Standard console.info
49 | */
50 | public debug(...props: Parameters): void {
51 | console.info(...applyChalkColor('green', props));
52 | }
53 |
54 | /**
55 | * Print a divider
56 | */
57 | public divide(): void {
58 | this.info('-----------------------------------');
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/generateMixins/helpers/associationDefinitionToSections.ts:
--------------------------------------------------------------------------------
1 | // mixins
2 | import {
3 | AssociationsDefinition,
4 | AssociationSection,
5 | GenerateMixinsConfig,
6 | } from '../types';
7 |
8 | // local
9 | import calculateMixinAttributes from './calculateMixinAttributes';
10 | import getKeys from './getKeys';
11 |
12 | /**
13 | * Convert an associations file definition to a list of the association type sections with their accompanied
14 | *
15 | * @param associationsDefinition - The section to process
16 | * @param config - The mixin generation config
17 | * @returns The association definitions
18 | */
19 | export default function associationDefinitionToSections(
20 | { associations }: AssociationsDefinition,
21 | config: GenerateMixinsConfig,
22 | ): AssociationSection[] {
23 | return (
24 | getKeys(associations)
25 | // Remove any associations that are empty objects
26 | .filter(
27 | (associationType) =>
28 | Object.values(associations[associationType]).length > 0,
29 | )
30 | // Construct the section config
31 | .map((associationType) => ({
32 | associationType,
33 | // Determine the association definitions
34 | definitions: getKeys(associations[associationType]).map(
35 | (associationName) => ({
36 | associationName,
37 | ...associations[associationType][associationName],
38 | // Determine the mixin attributes to inject
39 | attributes: calculateMixinAttributes(
40 | {
41 | associationName,
42 | ...associations[associationType][associationName],
43 | },
44 | associationType,
45 | config.getCustomMixinAttributes,
46 | config.pluralCase,
47 | ),
48 | }),
49 | ),
50 | }))
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/generateMixins/helpers/calculateMixinAttributes.ts:
--------------------------------------------------------------------------------
1 | // mixins
2 | import ChangeCase from '../ChangeCase';
3 | import { SEQUELIZE_MIXINS, WILDEBEEST_MIXINS } from '../constants';
4 | import {
5 | AssociationInput,
6 | AssociationMixins,
7 | AssociationType,
8 | MixinAttributeInput,
9 | MixinAttributes,
10 | } from '../types';
11 |
12 | /**
13 | * Get the typescript configuration in the directory
14 | *
15 | * @param association - The association definition to calculate the attributes for
16 | * @param associationType - The type of association to get the attributes for
17 | * @param additionMixins - Custom mixin attributes to inject
18 | * @param pluralCase - Optionally provide a custom pluralization function
19 | * @returns The tsconfig or throws an error
20 | */
21 | export default function calculateMixinAttributes(
22 | association: Omit,
23 | associationType: AssociationType,
24 | additionMixins: Partial = {},
25 | pluralCase?: (word: string) => string,
26 | ): MixinAttributes[] {
27 | const attributeInput: MixinAttributeInput = {
28 | modelName: new ChangeCase(association.modelName, pluralCase),
29 | associationName: new ChangeCase(association.associationName, pluralCase),
30 | primaryKeyName: association.primaryKeyName,
31 | throughModelName: association.throughModelName,
32 | };
33 |
34 | // Check if there are custom mixins
35 | const customMixins = additionMixins[associationType];
36 |
37 | // Calculate the mixins to inject
38 | const sequelizeMixins = SEQUELIZE_MIXINS[associationType](attributeInput);
39 | const wildebeestMixins = WILDEBEEST_MIXINS[associationType](attributeInput);
40 | const additionalMixins = customMixins ? customMixins(attributeInput) : [];
41 |
42 | // Merge them all together
43 | return [...sequelizeMixins, ...wildebeestMixins, ...additionalMixins];
44 | }
45 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tslint:recommended",
3 | "jsEnable": false,
4 | "rulesDirectory": ["node_modules/tslint-microsoft-contrib"],
5 | "esSpecCompliant": true,
6 | "rules": {
7 | "array-type": false,
8 | "camel-case": false,
9 | "completed-docs": [
10 | true,
11 | "classes",
12 | "enums",
13 | "enum-members",
14 | "functions",
15 | "interfaces",
16 | "methods",
17 | "namespaces",
18 | "properties",
19 | "types"
20 | ],
21 | "export-name": false,
22 | "interface-over-type-literal": false,
23 | "max-line-length": false,
24 | "new-parens": true,
25 | "prefer-for-of": false,
26 | "no-arg": true,
27 | "ordered-imports": true,
28 | "object-literal-key-quotes": false,
29 | "no-bitwise": true,
30 | "no-conditional-assignment": true,
31 | "no-consecutive-blank-lines": false,
32 | "no-console": false,
33 | "no-unused-expression": false,
34 | "no-namespace": false,
35 | "no-shadowed-variable": false,
36 | "no-var-requires": false,
37 | "object-literal-sort-keys": false,
38 | "quotemark": [false, "single", "avoid-escape", "avoid-template"],
39 | "variable-name": false,
40 | "trailing-comma": false,
41 | "semicolon": false
42 | },
43 | "jsRules": {
44 | "array-type": false,
45 | "align": false,
46 | "camel-case": false,
47 | "completed-docs": false,
48 | "max-line-length": false,
49 | "quotemark": [false, "single", "avoid-escape", "avoid-template"],
50 | "no-console": false,
51 | "object-literal-sort-keys": false,
52 | "no-shadowed-variable": false,
53 | "no-var-requires": false,
54 | "variable-name": false,
55 | "trailing-comma": false,
56 | "semicolon": false
57 | },
58 | "linterOptions": {
59 | "exclude": [
60 | "node_modules/**/*.ts",
61 | "dist/**/*.ts",
62 | "build/**/*.js",
63 | "builds/**/*.js"
64 | ]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # https://docs.travis-ci.com/user/languages/javascript-with-nodejs/
2 | language: node_js
3 |
4 | # Make sure that branch and PR triggers are enabled in Travis CI settings on the web UI, and then
5 | # it will run whenever a PR is opened and whenever a new commit is merged into one of these branches
6 | branches:
7 | only:
8 | - master
9 |
10 | git:
11 | depth: 2
12 |
13 | # Node version
14 | node_js: '12.14.0'
15 |
16 | before_install:
17 | - echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > "$HOME/.npmrc"
18 | - npm install -g yarn@1.22.4
19 | - python -m pip install --upgrade pip --user
20 | - pip install pre-commit --user
21 | - export PATH=$PATH:$HOME/.local/bin
22 |
23 | # Install top level dependencies
24 | install:
25 | - yarn
26 |
27 | # the following line is needed to enable the TravisCI build conditions
28 | conditions: v1
29 |
30 | # Run these commands
31 | jobs:
32 | include:
33 | # ##### #
34 | # Tests #
35 | # ##### #
36 |
37 | # Run pre-commit hooks on all files
38 | - stage: test
39 | name: Pre-Commits
40 | script:
41 | - pre-commit run --all-files
42 |
43 | # Run tests
44 | - stage: test
45 | name: Tests
46 | script: npm run test
47 |
48 | # ###### #
49 | # Deploy #
50 | # ###### #
51 |
52 | # Deploy to npm
53 | - stage: deploy
54 | script: skip
55 | deploy:
56 | - provider: npm
57 | email: '$NPM_EMAIL'
58 | api_key: '$NPM_TOKEN'
59 | skip_cleanup: true
60 | on:
61 | branch: master
62 |
63 | cache:
64 | directories:
65 | # Cache pip activities to speed up pre-commit installation
66 | - $HOME/.cache/pip
67 | - $HOME/.cache/pre-commit
68 | before_cache:
69 | - rm -f $HOME/.cache/pip/log/debug.log
70 | - rm -f $HOME/.cache/pre-commit/pre-commit.log
71 | - rm -f $HOME/.npm/anonymous-cli-metrics.json
72 |
73 | addons:
74 | apt:
75 | packages:
76 | - 'python'
77 |
--------------------------------------------------------------------------------
/src/checks/model.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import {
3 | ConfiguredModelDefinition,
4 | ModelMap,
5 | StringKeys,
6 | SyncError,
7 | } from '@wildebeest/types';
8 | import tableExists from '@wildebeest/utils/tableExists';
9 |
10 | // classes
11 | import Wildebeest from '@wildebeest/classes/Wildebeest';
12 |
13 | // local
14 | import checkAssociationsSync from './associations';
15 | import checkColumnDefinitions from './columnDefinitions';
16 | import checkIndexes from './indexes';
17 |
18 | /**
19 | * Check that a db model definition in Sequelize is in sync with the actual database definition
20 | *
21 | * @param wildebeest - The wildebeest configuration
22 | * @param model - The database model definition to verify
23 | * @param modelName - The name of the model
24 | * @returns Any errors related to the model definition
25 | */
26 | export default async function checkModel(
27 | wildebeest: Wildebeest,
28 | model: ConfiguredModelDefinition>,
29 | modelName: StringKeys,
30 | ): Promise {
31 | // Keep track of errors
32 | const errors: SyncError[] = [];
33 |
34 | // You can skip the check
35 | if (model.skip) {
36 | return errors;
37 | }
38 |
39 | // Ensure the table exist
40 | const exists = await tableExists(wildebeest.db, model.tableName);
41 | if (!exists) {
42 | errors.push({
43 | message: `Missing table: ${model.tableName}`,
44 | tableName: model.tableName,
45 | });
46 | }
47 |
48 | // Additional checks
49 | const allErrors = await Promise.all([
50 | // Check column definitions
51 | checkColumnDefinitions(wildebeest, model),
52 | // Ensure the table has the proper multi column indexes
53 | checkIndexes(wildebeest, model),
54 | // Ensure the associations are in sync
55 | checkAssociationsSync(wildebeest, model, modelName),
56 | ]);
57 |
58 | // If true, the model definition is in sync
59 | return errors.concat(...allErrors);
60 | }
61 |
--------------------------------------------------------------------------------
/src/migrationTypes/makeColumnUnique.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { MigrationDefinition, ModelMap } from '@wildebeest/types';
3 |
4 | /**
5 | * The type of constraint
6 | */
7 | export type ConstraintType = 'unique';
8 |
9 | /**
10 | * Options for making a column unique
11 | */
12 | export type MakeColumnUniqueOptions = {
13 | /** The name of the table to change the column on */
14 | tableName: string;
15 | /** The name of the column to make unique */
16 | columnName: string;
17 | /** Override the default constraint name */
18 | constraintName?: string;
19 | /** The type of constraint to make */
20 | constraintType?: ConstraintType;
21 | };
22 |
23 | /**
24 | * Make a column unique
25 | *
26 | * @param options - Options for making the table column unique
27 | * @returns The make column unique migrator
28 | */
29 | export default function makeColumnUnique(
30 | options: MakeColumnUniqueOptions,
31 | ): MigrationDefinition {
32 | const {
33 | tableName,
34 | columnName,
35 | constraintName,
36 | constraintType = 'unique',
37 | } = options;
38 | return {
39 | // Add the constraint
40 | up: async ({ db, namingConventions }, withTransaction) =>
41 | withTransaction((transactionOptions) =>
42 | db.queryInterface.addConstraint(tableName, [columnName], {
43 | type: constraintType,
44 | name:
45 | constraintName ||
46 | namingConventions.uniqueConstraint(tableName, columnName),
47 | ...transactionOptions,
48 | }),
49 | ),
50 | // Remove the constraint
51 | down: async ({ db, namingConventions }, withTransaction) =>
52 | withTransaction((transactionOptions) =>
53 | db.queryInterface.sequelize.query(
54 | `ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${
55 | constraintName ||
56 | namingConventions.uniqueConstraint(tableName, columnName)
57 | }";`,
58 | transactionOptions,
59 | ),
60 | ),
61 | };
62 | }
63 |
--------------------------------------------------------------------------------
/src/classes/WildebeestDb.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A modified sequelize database class with helper functions useful to wildebeest
3 | */
4 |
5 | // external
6 | import {
7 | DataTypes,
8 | Options,
9 | QueryInterface,
10 | Sequelize,
11 | Transaction,
12 | } from 'sequelize';
13 |
14 | // global
15 | import { ModelMap, StringKeys } from '@wildebeest/types';
16 |
17 | /**
18 | * A db model
19 | */
20 | export default class WildebeestDb extends Sequelize {
21 | /** No need to call getter to access queryInterface during migrations */
22 | public queryInterface: QueryInterface;
23 |
24 | /** The data types */
25 | public DataTypes = DataTypes;
26 |
27 | /** Db model needs to be fixed */
28 | public model!: >(
29 | modelName: TModelName,
30 | ) => TModels[TModelName];
31 |
32 | /** The uri of the database */
33 | public databaseUri: string;
34 |
35 | /**
36 | * Connect a global query interface
37 | *
38 | * @param databaseUri - The URI of the db
39 | * @param options - Additional options
40 | */
41 | public constructor(databaseUri: string, options?: Options) {
42 | super(databaseUri, options);
43 | this.databaseUri = databaseUri;
44 | this.queryInterface = this.getQueryInterface();
45 | }
46 |
47 | /**
48 | * Helper to continue transaction or create a new
49 | *
50 | * @param options - The transaction options
51 | * @param callback - The callback to execute in a transaction
52 | * @returns The callback result
53 | */
54 | public continueTransaction<
55 | TOptions extends
56 | | {
57 | /** The current transaction */
58 | transaction?: Transaction;
59 | }
60 | | undefined,
61 | T
62 | >(
63 | options: TOptions,
64 | callback: (t: Transaction) => PromiseLike,
65 | ): Promise {
66 | if (options && options.transaction) {
67 | return Promise.resolve(callback(options.transaction));
68 | }
69 | return this.transaction((t) => callback(t));
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/generators/migrationTypes/changeEnumAttributes/index.ts:
--------------------------------------------------------------------------------
1 | // local
2 | import { listEnumAttributes } from './lib';
3 |
4 | // migrationTypes
5 | import { SEQUELIZE_ENUM_NAME_REGEX } from '@generators//migrationTypes/tableColumnsMigration/lib';
6 | import { listAllAttributeConfigs } from '../tableColumnMigration/lib';
7 |
8 | let columnName;
9 | let useModelPath;
10 |
11 | /**
12 | * A higher order generator that creates a Change enum attributes migrator.
13 | *
14 | * Changing attributes of an enum
15 | *
16 | */
17 | export default {
18 | parentType: 'alterEnum',
19 | prompts: {
20 | attributes: (repo, { attributesSuggestOnly = false }) => ({
21 | message: 'Choose another attribute?',
22 | // Remove auto items when auto change on
23 | source: ({ enumName, modelPath }) => {
24 | // The name of the column
25 | if (!columnName) {
26 | columnName = enumName.split('_').pop();
27 | }
28 | if (!useModelPath) {
29 | useModelPath = modelPath;
30 | }
31 |
32 | // Get the attribute config
33 | const [
34 | {
35 | config: { content },
36 | relativePath,
37 | },
38 | ] = listAllAttributeConfigs(repo, useModelPath).filter(
39 | (attribute) => attribute.columnName === columnName,
40 | );
41 |
42 | // Determine the name of the enum
43 | if (!SEQUELIZE_ENUM_NAME_REGEX.test(content)) {
44 | throw new Error(`Unknown enum definition "${enumName}"`);
45 | }
46 | const [, enumInstanceName] = SEQUELIZE_ENUM_NAME_REGEX.exec(content);
47 |
48 | // Extract the export config of the enum
49 | const enumConfig = repo.getConfigForImport(
50 | relativePath,
51 | enumInstanceName,
52 | );
53 |
54 | // List the enum attributes
55 | return listEnumAttributes(enumConfig.content);
56 | },
57 | itemName: 'name',
58 | suggestOnly: attributesSuggestOnly,
59 | type: 'recurseWithoutRepeat',
60 | }),
61 | },
62 | };
63 |
--------------------------------------------------------------------------------
/fix_absolute_imports.js:
--------------------------------------------------------------------------------
1 | // external
2 | const { join } = require('path');
3 | const { readFileSync, writeFileSync } = require('fs');
4 | const glob = require('glob');
5 |
6 | // TODO take form checks
7 | const ROOTS = [__dirname];
8 |
9 | /**
10 | * Remove JSON Comments from tsconfig
11 | */
12 | function removeJSONComments(jsonString) {
13 | return jsonString.replace(/\/\/.*?\n|\/\* .*?\n/g, '');
14 | }
15 |
16 | /**
17 | * Run fix
18 | */
19 | function fix(fil, dist, compilerOptions) {
20 | // Read in the file contents
21 | let fileData = readFileSync(fil, 'utf-8');
22 |
23 | // Determine the relative path of the file to the build base
24 | const diff = fil.replace(`${dist}/`, '');
25 |
26 | // Determine relative pass to build
27 | const relativeToBuild = '../'.repeat(diff.split('/').length - 1) || './';
28 |
29 | Object.entries(compilerOptions.paths).forEach(([key, [rep]]) => {
30 | // The require statements to replace
31 | const keyPrefix = key.split('/*')[0];
32 |
33 | // Create a regex to find all matches
34 | const MATCH_REGEX = new RegExp(`${keyPrefix}/`, 'g');
35 |
36 | // Determine what to replace
37 | let path = rep.split('/*')[0];
38 | if (path.startsWith('node_modules/')) {
39 | path = path.replace('node_modules/', '');
40 | if (!path.endsWith('/')) {
41 | path += '/';
42 | }
43 | } else if (path.includes('build')) {
44 | path = `${relativeToBuild}${path}/`;
45 | } else {
46 | path = relativeToBuild;
47 | }
48 |
49 | fileData = fileData.replace(MATCH_REGEX, path);
50 | });
51 |
52 | writeFileSync(fil, fileData, 'utf-8');
53 | }
54 |
55 | /**
56 | * Convert build
57 | */
58 | function convertBuild(rootPath) {
59 | const { compilerOptions } = JSON.parse(
60 | removeJSONComments(readFileSync(join(rootPath, 'tsconfig.json'), 'utf-8')),
61 | );
62 | const dist = join(rootPath, compilerOptions.outDir, 'src');
63 | const files = glob.sync(`${dist}/**/*.js`);
64 | const dTsFiles = glob.sync(`${dist}/**/*.d.ts`);
65 | [...files, ...dTsFiles].map((fil) => fix(fil, dist, compilerOptions));
66 | }
67 |
68 | ROOTS.map(convertBuild);
69 |
--------------------------------------------------------------------------------
/src/migrationTypes/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Helper functions that will create a specific type of database migration.
3 | */
4 |
5 | // local
6 | export { default as addCascadeWithParent } from './addCascadeWithParent';
7 | export { default as addColumns } from './addColumns';
8 | export { default as addEnumValues } from './addEnumValues';
9 | export { default as addIndex } from './addIndex';
10 | export { default as addUniqueConstraintIndex } from './addUniqueConstraintIndex';
11 | export { default as cascadeWithParent } from './cascadeWithParent';
12 | export { default as changeColumn } from './changeColumn';
13 | export { default as changeColumnDefault } from './changeColumnDefault';
14 | export { default as changeEnumColumn } from './changeEnumColumn';
15 | export { default as changeOnDelete } from './changeOnDelete';
16 | export { default as custom } from './custom';
17 | export { default as convertEnumValues } from './convertEnumValues';
18 | export { default as createTable } from './createTable';
19 | export { default as dropTable } from './dropTable';
20 | export { default as init } from './init';
21 | export { default as makeColumnAllowNull } from './makeColumnAllowNull';
22 | export { default as makeColumnNonNull } from './makeColumnNonNull';
23 | export { default as makeColumnNotUnique } from './makeColumnNotUnique';
24 | export { default as makeColumnUnique } from './makeColumnUnique';
25 | export { default as removeColumns } from './removeColumns';
26 | export { default as removeEnumValues } from './removeEnumValues';
27 | export { default as removeIndex } from './removeIndex';
28 | export { default as removeNonNullColumn } from './removeNonNullColumn';
29 | export { default as removeUniqueConstraintIndex } from './removeUniqueConstraintIndex';
30 | export { default as renameColumn } from './renameColumn';
31 | export { default as renameConstraint } from './renameConstraint';
32 | export { default as renameEnum } from './renameEnum';
33 | export { default as renameEnumValue } from './renameEnumValue';
34 | export { default as renameIndex } from './renameIndex';
35 | export { default as renameS3Files } from './renameS3Files';
36 | export { default as renameTable } from './renameTable';
37 | export { default as skip } from './skip';
38 | export { default as uuidNonNull } from './uuidNonNull';
39 |
--------------------------------------------------------------------------------
/generators/migrations/index.ts:
--------------------------------------------------------------------------------
1 | // local
2 | import addCascadeWithParent from './addCascadeWithParent';
3 | import addColumns from './addColumns';
4 | import addEnumValues from './addEnumValues';
5 | import addIndex from './addIndex';
6 | import addUniqueConstraintIndex from './addUniqueConstraintIndex';
7 | import bulkDelete from './bulkDelete';
8 | import bulkInsert from './bulkInsert';
9 | import cascadeWithParent from './cascadeWithParent';
10 | import changeColumn from './changeColumn';
11 | import changeEnumColumn from './changeEnumColumn';
12 | import changeOnDelete from './changeOnDelete';
13 | import createTable from './createTable';
14 | import custom from './custom';
15 | import dropTable from './dropTable';
16 | import makeColumnAllowNull from './makeColumnAllowNull';
17 | import makeColumnNonNull from './makeColumnNonNull';
18 | import makeColumnUnique from './makeColumnUnique';
19 | import removeColumns from './removeColumns';
20 | import removeEnumValues from './removeEnumValues';
21 | import removeIndex from './removeIndex';
22 | import removeNonNullColumn from './removeNonNullColumn';
23 | import removeUniqueConstraintIndex from './removeUniqueConstraintIndex';
24 | import renameEnumValue from './rename-enum-value';
25 | import renameColumn from './renameColumn';
26 | import renameConstraint from './renameConstraint';
27 | import renameEnum from './renameEnum';
28 | import renameIndex from './renameIndex';
29 | import renameS3Files from './renameS3Files';
30 | import renameTable from './renameTable';
31 | import uuidNonNull from './uuidNonNull';
32 |
33 | /**
34 | * Migration generators
35 | */
36 | export default {
37 | addCascadeWithParent,
38 | addColumns,
39 | addEnumValues,
40 | addIndex,
41 | addUniqueConstraintIndex,
42 | bulkDelete,
43 | bulkInsert,
44 | cascadeWithParent,
45 | changeColumn,
46 | changeEnumColumn,
47 | changeOnDelete,
48 | createTable,
49 | custom,
50 | dropTable,
51 | makeColumnAllowNull,
52 | makeColumnNonNull,
53 | makeColumnUnique,
54 | removeColumns,
55 | removeEnumValues,
56 | removeIndex,
57 | removeNonNullColumn,
58 | removeUniqueConstraintIndex,
59 | renameColumn,
60 | renameConstraint,
61 | renameEnum,
62 | renameEnumValue,
63 | renameIndex,
64 | renameS3Files,
65 | renameTable,
66 | uuidNonNull,
67 | };
68 |
--------------------------------------------------------------------------------
/generators/migrations/renameS3Files/index.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { NOT_EMPTY_REGEX } from '@generators/regexes';
3 |
4 | const getAttributes = (attributes): string[] =>
5 | attributes
6 | .split(',')
7 | .map((attr) => attr.trim())
8 | .map((attr) =>
9 | attr.startsWith('"') && attr.endsWith('')
10 | ? attr.slice(1, attr.length - 1)
11 | : attr,
12 | );
13 |
14 | /**
15 | * Change the s3 file naming convention for a table
16 | */
17 |
18 | export default {
19 | configure: ({ modelTableName, nameExt, model }) => ({
20 | name: `rename-s3-files-${modelTableName}-${nameExt}`,
21 | comment: `Change the naming convention of s3 files in ${model}`,
22 | }),
23 | description: 'Change the s3 file naming convention for a table',
24 | prompts: {
25 | nameExt: {
26 | message:
27 | 'Name the migration (will be appended after "rename-s3-files-{{ modelTableName }}-"',
28 | type: 'name',
29 | },
30 | bucket: {
31 | message: "What is the bucket? AWS_BUCKET['??']",
32 | type: 'input',
33 | validate: (value) =>
34 | NOT_EMPTY_REGEX.test(value) ? true : 'bucket is required',
35 | },
36 | attributes: {
37 | default: 'id',
38 | message:
39 | 'What attributes should be fetched for each row of the table, used to determine its old and new keys?',
40 | type: 'input',
41 | validate: (value) =>
42 | NOT_EMPTY_REGEX.test(value) ? true : 'attributes is required',
43 | },
44 | getOldKey: {
45 | default: ({ attributes }) =>
46 | `({ ${getAttributes(attributes).join(', ')} }) => TODO`,
47 | message:
48 | 'What is the function that will get the old key from a row in the table?',
49 | extension: 'hbs',
50 | type: 'editor',
51 | },
52 | getNewKey: {
53 | default: ({ attributes }) =>
54 | `({ ${getAttributes(attributes).join(', ')} }) => TODO`,
55 | message:
56 | 'What is the function that will get the new key from a row in the table?',
57 | extension: 'hbs',
58 | type: 'editor',
59 | },
60 | remove: {
61 | default: false,
62 | message: 'Remove rows that are missing an s3 file at oldKey?',
63 | type: 'confirm',
64 | },
65 | },
66 | type: 'tableMigration',
67 | };
68 |
--------------------------------------------------------------------------------
/src/checks/columnDefinitions.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import difference from 'lodash/difference';
3 | import uniq from 'lodash/uniq';
4 |
5 | // global
6 | import {
7 | ConfiguredModelDefinition,
8 | ModelMap,
9 | StringKeys,
10 | SyncError,
11 | } from '@wildebeest/types';
12 | import expectedColumnNames from '@wildebeest/utils/expectedColumnNames';
13 | import listColumns from '@wildebeest/utils/listColumns';
14 |
15 | // classes
16 | import Wildebeest from '@wildebeest/classes/Wildebeest';
17 |
18 | // local
19 | import checkColumnDefinition from './columnDefinition';
20 |
21 | /**
22 | * Check that all db model attributes defined by sequelize match the ones in postgres
23 | *
24 | * @param wildebeest - The wildebeest configuration
25 | * @param model - The db model to check against
26 | * @returns Any errors related to all column definitions in the table
27 | */
28 | export default async function checkColumnDefinitions(
29 | wildebeest: Wildebeest,
30 | model: ConfiguredModelDefinition>,
31 | ): Promise {
32 | // Keep track of errors
33 | const errors: SyncError[] = [];
34 |
35 | // Compare columns to what exist
36 | const expectedColumns = expectedColumnNames(
37 | model.attributes,
38 | model.rawAssociations,
39 | );
40 | const existingColumns = await listColumns(wildebeest.db, model.tableName);
41 |
42 | // Extra check
43 | const extraColumns = difference(existingColumns, expectedColumns);
44 | if (extraColumns.length > 0) {
45 | errors.push({
46 | message: `Extra columns: "${extraColumns.join('", "')}" in table: ${
47 | model.tableName
48 | }`,
49 | tableName: model.tableName,
50 | });
51 | }
52 |
53 | // Missing check
54 | const missingColumns = difference(expectedColumns, existingColumns);
55 | if (missingColumns.length > 0) {
56 | errors.push({
57 | message: `Missing columns: "${uniq(missingColumns).join(
58 | '", "',
59 | )}" in table: ${model.tableName}`,
60 | tableName: model.tableName,
61 | });
62 | }
63 |
64 | // Check each individual column definition
65 | const columnErrors = await Promise.all(
66 | existingColumns.map((name) =>
67 | checkColumnDefinition(wildebeest, model, name),
68 | ),
69 | );
70 |
71 | return errors.concat(...columnErrors);
72 | }
73 |
--------------------------------------------------------------------------------
/src/utils/createAssociationApply.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import {
3 | BelongsToAssociation,
4 | HasManyAssociation,
5 | HasOneAssociation,
6 | ObjByString,
7 | } from '@wildebeest/types';
8 |
9 | // local
10 | import apply, { ApplyFunc } from './apply';
11 |
12 | /**
13 | * Function to apply over associations and enforce
14 | */
15 | type ApplyFuncForAssociation = <
16 | TInput extends ObjByString,
17 | TOutput extends TOutputBase
18 | >(
19 | obj: TInput,
20 | applyFunc: ApplyFunc,
21 | ) => { [key in keyof TInput]: TOutput };
22 |
23 | /**
24 | * Apply functions with association
25 | */
26 | type AssociationApply = {
27 | /** Enforce a belongsTo association */
28 | belongsTo: ApplyFuncForAssociation>;
29 | /** Enforce a hasMany association */
30 | hasMany: ApplyFuncForAssociation>;
31 | /** Enforce a hasOne association */
32 | hasOne: ApplyFuncForAssociation>;
33 | };
34 |
35 | /**
36 | * Creates a new index, main purpose is to override @types/sequelize
37 | *
38 | * @returns A set of apply functions that will enforce association types without casting their underlying values
39 | */
40 | export default function createAssociationApply<
41 | TDatabaseModelName extends string
42 | >(): AssociationApply {
43 | const asBelongsTo = <
44 | TBelongsTo extends BelongsToAssociation
45 | >(
46 | x: TBelongsTo,
47 | ): TBelongsTo => x;
48 |
49 | const asHasMany = >(
50 | x: THasMany,
51 | ): THasMany => x;
52 |
53 | const asHasOne = >(
54 | x: THasOne,
55 | ): THasOne => x;
56 |
57 | return {
58 | belongsTo: (obj, applyFunc) =>
59 | apply(obj, (value, key, fullObj, ind) =>
60 | asBelongsTo(applyFunc(value, key, fullObj, ind)),
61 | ),
62 | hasMany: (obj, applyFunc) =>
63 | apply(obj, (value, key, fullObj, ind) =>
64 | asHasMany(applyFunc(value, key, fullObj, ind)),
65 | ),
66 | hasOne: (obj, applyFunc) =>
67 | apply(obj, (value, key, fullObj, ind) =>
68 | asHasOne(applyFunc(value, key, fullObj, ind)),
69 | ),
70 | };
71 | }
72 |
--------------------------------------------------------------------------------
/src/utils/getNumberedList.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Numbered Folder
3 | *
4 | * A regex to check for a filename in the form xxxx-name
5 | *
6 | * i.e. 0032-my-migration
7 | */
8 | export const NUMBERED_REGEX = /^(\d\d\d\d)-+[\w-]+(|\.js|\.ts)$/;
9 |
10 | /**
11 | * Helper function to parse out the number from a string input
12 | *
13 | * @param item - The item to pull the number from
14 | * @returns The number for that string
15 | */
16 | export const parseNumber = (item: string): number => {
17 | if (!NUMBERED_REGEX.test(item)) {
18 | throw new Error(`Item does not match regex: "${item}"`);
19 | }
20 |
21 | return parseInt((NUMBERED_REGEX.exec(item) || [])[1], 10);
22 | };
23 |
24 | /**
25 | * Verify that there are no duplicate numbered files and they are ordered 1, 2, ... N
26 | *
27 | * ```typescript
28 | * // Returns true
29 | * verifyNumberedFiles(['0001-file.ts', '0002-file2.ts']);
30 | * ```
31 | *
32 | * @throws {Error} If migration files are not ordered properly
33 | * @param files - The list of files to verify
34 | * @param throwError - Throw an error if invalid
35 | * @returns When throwError is false and there is an error, this will return what index the conflicts exist at
36 | */
37 | export function verifyNumberedFiles(files: string[], bottom = 1): number {
38 | const invalidIndex = -1;
39 |
40 | // Pull off the number
41 | files
42 | .map((fil) => fil.split('-')[0])
43 | // Ensure that the numbers are increasing in order
44 | .forEach((num, ind) => {
45 | if (parseInt(num, 10) !== ind + bottom) {
46 | console.log(parseInt(num, 10), ind + 1);
47 | throw new Error(
48 | `Migration file naming convention wrong at ${ind + bottom}`,
49 | );
50 | }
51 | });
52 |
53 | // Return the invalid index if not throwing an error
54 | return invalidIndex;
55 | }
56 |
57 | /**
58 | * Given a list of potential numbered files or folders (i.e. migrations), filter for those that follow the number pattern
59 | *
60 | * @param listItems - The raw list items to filter
61 | * @returns The numbered items in order
62 | */
63 | export default function getNumberedList(
64 | listItems: string[],
65 | bottom = 1,
66 | regex = NUMBERED_REGEX,
67 | ): string[] {
68 | // Filter by regex
69 | const migrations = listItems.filter((item) => regex.test(item));
70 |
71 | // Verify that they are valid
72 | verifyNumberedFiles(migrations, bottom);
73 |
74 | return migrations;
75 | }
76 |
--------------------------------------------------------------------------------
/src/utils/setAssociations.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | // classes
3 | import WildebeestModel from '@wildebeest/classes/WildebeestModel';
4 |
5 | // local
6 | import apply from './apply';
7 |
8 | /**
9 | * Setup the associations for a db model, saving the association to the class definition of the model
10 | *
11 | * TODO Do not set names of model associations dynamically
12 | *
13 | * @param Model - The model to set the associations for
14 | * @returns Returns the model instance with associations set
15 | */
16 | export default function setAssociations(
17 | Model: T,
18 | ): T {
19 | // Grab the associations
20 | const { wildebeest, db, configuredDefinition } = Model;
21 |
22 | // Ensure db is initialized beforehand
23 | if (!db || !wildebeest || !configuredDefinition) {
24 | throw new Error(
25 | `Db model must be initialized before calling "setAssociations"`,
26 | );
27 | }
28 | const { associations } = configuredDefinition;
29 |
30 | // Process `hasMany` associations
31 | apply(associations.hasMany, (association, associationName) => {
32 | // Get the child model
33 | const childModel = db.model(association.modelName);
34 |
35 | // Has many relation
36 | Model.associations[wildebeest.pluralCase(associationName)] = Model.hasMany(
37 | childModel,
38 | association,
39 | );
40 | });
41 |
42 | // Process `hasOne` associations
43 | apply(associations.hasOne, (association, associationName) => {
44 | // Get the child model
45 | const childModel = db.model(association.modelName);
46 |
47 | // Has one child relation
48 | Model.associations[associationName] = Model.hasOne(childModel, association);
49 | });
50 |
51 | // Process `belongsTo` associations
52 | apply(associations.belongsTo, (association, associationName) => {
53 | // Get the child model
54 | const parentModel = db.model(association.modelName);
55 |
56 | // Belongs to
57 | Model.associations[associationName] = Model.belongsTo(
58 | parentModel,
59 | association,
60 | );
61 | });
62 |
63 | // Process `belongsToMany` associations
64 | apply(associations.belongsToMany, (association, associationName) => {
65 | // Get the child model
66 | const associationModel = db.model(associationName);
67 |
68 | // Belongs to
69 | Model.associations[
70 | wildebeest.pluralCase(associationName)
71 | ] = Model.belongsToMany(associationModel, association);
72 | });
73 |
74 | return Model;
75 | }
76 |
--------------------------------------------------------------------------------
/src/utils/addTableColumnConstraint.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import sequelize from 'sequelize';
3 |
4 | // global
5 | import Wildebeest from '@wildebeest/classes/Wildebeest';
6 | import {
7 | Attributes,
8 | MigrationTransactionOptions,
9 | ModelMap,
10 | } from '@wildebeest/types';
11 | import inferTableReference from '@wildebeest/utils/inferTableReference';
12 |
13 | /**
14 | * Options for adding a constraint to a table
15 | */
16 | export type AddTableConstraintOptions = {
17 | /** The name of the table */
18 | tableName: string;
19 | /** The name of the column */
20 | columnName: string;
21 | /** The add constraint options to pass to queryInterface.addConstraint */
22 | constraintOptions: sequelize.AddForeignKeyConstraintOptions;
23 | /** Override the name of the constraint */
24 | constraintName?: string;
25 | /** When true, drop the constraint if it exists */
26 | drop?: boolean;
27 | };
28 |
29 | /**
30 | * Add a constraint on a table column
31 | *
32 | * @param db - The database to add the table constraint to
33 | * @param options - The add constraint options
34 | * @param rawTransactionOptions - The current transaction
35 | * @returns The create constraint promise
36 | */
37 | export default async function addTableColumnConstraint<
38 | TModels extends ModelMap,
39 | TAttributes extends Attributes
40 | >(
41 | { db, namingConventions, pluralCase }: Wildebeest,
42 | options: AddTableConstraintOptions,
43 | transactionOptions: MigrationTransactionOptions,
44 | ): Promise {
45 | // Raw query interface
46 | const { queryInterface } = db;
47 | const { queryT } = transactionOptions;
48 | const {
49 | tableName,
50 | columnName,
51 | constraintOptions,
52 | constraintName,
53 | drop = false,
54 | } = options;
55 | const { references, ...otherConstraintOptions } = constraintOptions;
56 |
57 | // Determine the name of the constraint
58 | const useConstraintName =
59 | constraintName ||
60 | namingConventions.foreignKeyConstraint(tableName, columnName);
61 |
62 | // Drop the constraint first
63 | if (drop) {
64 | await queryT.raw(
65 | `ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${useConstraintName}";`,
66 | );
67 | }
68 | // Add the new constraint
69 | await queryInterface.addConstraint(tableName, [columnName], {
70 | name: useConstraintName,
71 | references: references || inferTableReference(columnName, pluralCase),
72 | ...otherConstraintOptions,
73 | ...transactionOptions,
74 | });
75 | }
76 |
--------------------------------------------------------------------------------
/src/utils/migrateEnumValues.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import Wildebeest from '@wildebeest/classes/Wildebeest';
3 | import {
4 | Attributes,
5 | MigrationTransactionOptions,
6 | ModelMap,
7 | } from '@wildebeest/types';
8 |
9 | // local
10 | import columnAllowsNull from './columnAllowsNull';
11 | import getColumnDefault from './getColumnDefault';
12 | import migrateEnumColumn, { MigrateEnumOptions } from './migrateEnumColumn';
13 |
14 | /**
15 | * Lookup the current column attributes, and migrate the values of an enum by removing the old enum and adding a new enum.
16 | *
17 | * This is done to preserve proper non-superuser postgres admin privilege during migrations.
18 | *
19 | * @param tableName - The name of the table
20 | * @param columnName - The name of the column where enum is applied
21 | * @param enumValueList - The new enum values
22 | * @param options - Specify additional options to pass to migrateEnumColumn
23 | * @returns The change enum promise
24 | */
25 | export default async function migrateEnumValues(
26 | wildebeest: Wildebeest,
27 | enumValueList: string[],
28 | migrateEnumOptions: MigrateEnumOptions,
29 | transactionOptions: MigrationTransactionOptions,
30 | ): Promise {
31 | const { tableName, columnName } = migrateEnumOptions;
32 |
33 | // Build the new enum values
34 | const enumValue = enumValueList.reduce(
35 | (acc, val) => Object.assign(acc, { [val]: val }),
36 | {},
37 | );
38 |
39 | // Keep the same allowNull and defaultValue
40 | const [allowNull, defaultValue] = await Promise.all([
41 | columnAllowsNull(wildebeest.db, tableName, columnName, transactionOptions),
42 | getColumnDefault(wildebeest.db, tableName, columnName, transactionOptions),
43 | ]);
44 |
45 | // Determine the new default value (it is either the existing defaultValue or the converted default value)
46 | const calculatedDefaultValue =
47 | defaultValue && defaultValue.includes('::')
48 | ? defaultValue.split('::')[0].split("'").join('')
49 | : '';
50 |
51 | // Check if the default value needs to be converted
52 | const { convertEnum = {} } = migrateEnumOptions;
53 | const convertedDefaultValue =
54 | convertEnum[calculatedDefaultValue] || calculatedDefaultValue;
55 |
56 | // Migrate the column
57 | await migrateEnumColumn(
58 | wildebeest,
59 | enumValue,
60 | {
61 | ...migrateEnumOptions,
62 | allowNull,
63 | defaultValue: convertedDefaultValue,
64 | },
65 | transactionOptions,
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/src/migrationTypes/cascadeWithParent.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { OnDelete } from '@wildebeest/enums';
3 | import { MigrationDefinition, ModelMap } from '@wildebeest/types';
4 | import addTableColumnConstraint from '@wildebeest/utils/addTableColumnConstraint';
5 | import { TableReference } from '@wildebeest/utils/inferTableReference';
6 |
7 | /**
8 | * Options for changing the cascade on a foreign key
9 | */
10 | export type CascadeWithParentOptions = {
11 | /** The name of the table to add the cascade on */
12 | tableName: string;
13 | /** The name of the column on the table that should be cascading */
14 | columnName: string;
15 | /** The references for the table where the constraint is made */
16 | references?: TableReference;
17 | /** The name of the constraint, if left null will be inferred */
18 | constraintName?: string;
19 | /** What should occur when parent is deleted */
20 | onDelete?: OnDelete;
21 | };
22 |
23 | /**
24 | * Update a column to have an on delete cascade when it's parent is cascaded
25 | *
26 | * @param options - The add cascade with parent migrator options
27 | * @returns The cascade with parent migrator
28 | */
29 | export default function cascadeWithParent(
30 | options: CascadeWithParentOptions,
31 | ): MigrationDefinition {
32 | const {
33 | tableName,
34 | columnName,
35 | references,
36 | constraintName,
37 | onDelete = 'cascade',
38 | } = options;
39 | return {
40 | up: async (wildebeest, withTransaction) =>
41 | withTransaction((transactionOptions) =>
42 | addTableColumnConstraint(
43 | wildebeest,
44 | {
45 | tableName,
46 | columnName,
47 | constraintOptions: {
48 | type: 'foreign key',
49 | onDelete,
50 | onUpdate: 'cascade',
51 | references,
52 | },
53 | constraintName,
54 | drop: true,
55 | },
56 | transactionOptions,
57 | ),
58 | ),
59 | down: async (wildebeest, withTransaction) =>
60 | withTransaction((transactionOptions) =>
61 | addTableColumnConstraint(
62 | wildebeest,
63 | {
64 | tableName,
65 | columnName,
66 | constraintOptions: {
67 | type: 'foreign key',
68 | onDelete: 'set null',
69 | onUpdate: 'cascade',
70 | references,
71 | },
72 | constraintName,
73 | drop: true,
74 | },
75 | transactionOptions,
76 | ),
77 | ),
78 | };
79 | }
80 |
--------------------------------------------------------------------------------
/src/checks/uniqueConstraint.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import { ModelAttributeColumnOptions, QueryTypes } from 'sequelize';
3 |
4 | // global
5 | import Wildebeest from '@wildebeest/classes/Wildebeest';
6 | import WildebeestDb from '@wildebeest/classes/WildebeestDb';
7 | import { ModelMap, SyncError } from '@wildebeest/types';
8 |
9 | /**
10 | * Check if the database has a unique constraint
11 | *
12 | * @param db - The db instance
13 | * @param tableName - The name of the table
14 | * @param name - The name of the constraint
15 | * @returns True if the constraint is defined
16 | */
17 | export async function hasUniqueConstraint(
18 | db: WildebeestDb,
19 | tableName: string,
20 | name: string,
21 | ): Promise {
22 | const [constraint] = await db.queryInterface.sequelize.query(
23 | `
24 | SELECT constraint_name,table_name
25 | FROM information_schema.constraint_column_usage
26 | WHERE table_name = '${tableName}' and "constraint_name" = '${name}'
27 | `,
28 | {
29 | type: QueryTypes.SELECT,
30 | },
31 | );
32 | return !!constraint;
33 | }
34 |
35 | /**
36 | * Ensure a unique constraint is set properly
37 | *
38 | * @param model - The db model to check
39 | * @param name - The name of the attribute
40 | * @param definition - The attribute definition
41 | * @returns Any errors related to unique constraints
42 | */
43 | export default async function checkUniqueConstraint(
44 | { namingConventions, db }: Wildebeest,
45 | tableName: string,
46 | name: string,
47 | definition: ModelAttributeColumnOptions,
48 | ): Promise {
49 | // Keep track of errors
50 | const errors: SyncError[] = [];
51 |
52 | // Check if expected to be unique
53 | const isUnique = !!definition.unique && !definition.primaryKey;
54 |
55 | // The name of the constraint
56 | const uniqueConstraintName = namingConventions.uniqueConstraint(
57 | tableName,
58 | name,
59 | );
60 |
61 | // Check if the constraint exists
62 | const constraintExists = await hasUniqueConstraint(
63 | db,
64 | tableName,
65 | uniqueConstraintName,
66 | );
67 |
68 | // Determine if a constraint exists when it should not
69 | if (!isUnique && constraintExists) {
70 | errors.push({
71 | message: `Has unexpected constraint: "${uniqueConstraintName}"`,
72 | tableName,
73 | });
74 | }
75 |
76 | // Determine if a constraint does not exist when expected
77 | if (isUnique && !constraintExists) {
78 | errors.push({
79 | message: `Missing expected constraint: "${uniqueConstraintName}"`,
80 | tableName,
81 | });
82 | }
83 |
84 | return errors;
85 | }
86 |
--------------------------------------------------------------------------------
/src/migrationTypes/addCascadeWithParent.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import { OnDelete } from '@wildebeest/enums';
3 | import { MigrationDefinition, ModelMap } from '@wildebeest/types';
4 | import addTableColumnConstraint from '@wildebeest/utils/addTableColumnConstraint';
5 | import { TableReference } from '@wildebeest/utils/inferTableReference';
6 |
7 | /**
8 | * Options for making a column cascade with its parent
9 | */
10 | export type CascadeWithParentOptions = {
11 | /** The name of the table to add the cascade to */
12 | tableName: string;
13 | /** The name of the column in the table that needs the constraint */
14 | columnName: string;
15 | /** Specify the foreign key constraint manually */
16 | references?: TableReference;
17 | /** The name of the constraint, if left null will be inferred */
18 | constraintName?: string;
19 | /** The on delete setting for the constraint */
20 | onDelete?: OnDelete;
21 | };
22 |
23 | /**
24 | * Add a constraint to a column to have an on delete cascade when it's parent is cascaded
25 | *
26 | * @param options - The add cascade with parent migrator options
27 | * @returns The add cascade with parent migrator
28 | */
29 | export default function addCascadeWithParent(
30 | options: CascadeWithParentOptions,
31 | ): MigrationDefinition {
32 | const {
33 | tableName,
34 | columnName,
35 | references,
36 | constraintName,
37 | onDelete = 'CASCADE',
38 | } = options;
39 | return {
40 | // Add the constraint
41 | up: async (wildebeest, withTransaction) =>
42 | withTransaction((transactionOptions) =>
43 | addTableColumnConstraint(
44 | wildebeest,
45 | {
46 | tableName,
47 | columnName,
48 | constraintOptions: {
49 | type: 'foreign key',
50 | onDelete,
51 | onUpdate: 'CASCADE',
52 | references,
53 | },
54 | constraintName:
55 | constraintName ||
56 | wildebeest.namingConventions.foreignKeyConstraint(
57 | tableName,
58 | columnName,
59 | ),
60 | },
61 | transactionOptions,
62 | ),
63 | ),
64 | // Remove the constraint
65 | down: async (wildebeest, withTransaction) =>
66 | withTransaction(({ queryT }) =>
67 | queryT.raw(
68 | `ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${
69 | constraintName ||
70 | wildebeest.namingConventions.foreignKeyConstraint(
71 | tableName,
72 | columnName,
73 | )
74 | }";`,
75 | ),
76 | ),
77 | };
78 | }
79 |
--------------------------------------------------------------------------------
/src/checks/belongsToAssociation.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import Wildebeest from '@wildebeest/classes/Wildebeest';
3 | import {
4 | BelongsToAssociation,
5 | ModelMap,
6 | StringKeys,
7 | SyncError,
8 | } from '@wildebeest/types';
9 |
10 | /**
11 | * Validate a single belongsTo association
12 | *
13 | * @param wildebeest - The wildebeest configuration
14 | * @param modelName - The name of the model being checked
15 | * @param tableName - The table of the model being checked
16 | * @param association - The association configuration
17 | * @param name - The name of the association
18 | * @returns True if belongs to association is valiid
19 | */
20 | export default function checkBelongsToAssociation(
21 | wildebeest: Wildebeest,
22 | modelName: StringKeys,
23 | tableName: string,
24 | association: BelongsToAssociation>,
25 | associationName: string,
26 | ): SyncError[] {
27 | // Keep track of errors
28 | const errors: SyncError[] = [];
29 |
30 | // The name of the current association
31 | const associationModelName =
32 | typeof association === 'object' && association.modelName
33 | ? association.modelName
34 | : (associationName as StringKeys);
35 |
36 | // Get the associations of the association
37 | const {
38 | associations = {},
39 | dontMatchBelongsTo,
40 | } = wildebeest.getModelDefinition(associationModelName);
41 |
42 | // Can skip if dont need to match it
43 | if (dontMatchBelongsTo) {
44 | return errors;
45 | }
46 |
47 | const { hasOne = {}, hasMany = {} } = associations;
48 |
49 | // Check if there is a hasOne or a hasMany
50 | const hasOneAssociation =
51 | hasOne[modelName] ||
52 | Object.values(hasOne).find(
53 | (a) => typeof a === 'object' && a.modelName === modelName,
54 | );
55 | const hasManyAssociation =
56 | hasMany[modelName] ||
57 | Object.values(hasMany).find(
58 | (a) => typeof a === 'object' && a.modelName === modelName,
59 | );
60 |
61 | // Log errors
62 | const possibleOpposites = 'hasOne or hasMany';
63 | const identifier = `Model "${associationModelName}"${
64 | associationName === associationModelName ? '' : ` (as "${associationName}")`
65 | }`;
66 | if (hasOneAssociation && hasManyAssociation) {
67 | errors.push({
68 | message: `${identifier} has multiple ${possibleOpposites} associations to "${modelName}"`,
69 | tableName,
70 | });
71 | } else if (!hasOneAssociation && !hasManyAssociation) {
72 | errors.push({
73 | message: `${identifier} is missing a ${possibleOpposites} association to "${modelName}"`,
74 | tableName,
75 | });
76 | }
77 | return errors;
78 | }
79 |
--------------------------------------------------------------------------------
/generateMixins/helpers/serializeAssociationType.ts:
--------------------------------------------------------------------------------
1 | // external
2 | import get from 'lodash/get';
3 |
4 | // global
5 | import { AssociationDefinition } from '../types';
6 |
7 | /**
8 | * Get the value of a string from an object
9 | */
10 | export function getValueFromObject(
11 | innerMap: any,
12 | attributeName: string,
13 | ): string | undefined {
14 | if (!innerMap) {
15 | return undefined;
16 | }
17 |
18 | // Get the definition
19 | const attributeObj = innerMap.get(attributeName);
20 | if (!attributeObj) {
21 | return undefined;
22 | }
23 |
24 | // Determine what the provided modelName is
25 | const attributeDefinition = get(
26 | attributeObj,
27 | 'valueDeclaration.initializer.text',
28 | );
29 | if (typeof attributeDefinition !== 'string') {
30 | return undefined;
31 | }
32 |
33 | return attributeDefinition;
34 | }
35 |
36 | /**
37 | * Process a member of the type definition (known types in the case of associations)
38 | *
39 | * @param member - The association type to process (i.e. inner symbol for `belongsTo`)
40 | * @returns A mapping from association name to model name
41 | */
42 | export default function serializeAssociationType(
43 | member: any,
44 | ): { [k in string]: AssociationDefinition } {
45 | // Grab the child types
46 | const innerMembers = get(member, 'type.members');
47 |
48 | // If there are no members return empty object
49 | if (!innerMembers) {
50 | return {};
51 | }
52 | const results: { [k in string]: AssociationDefinition } = {};
53 | innerMembers.forEach(({ type }: any, key: string) => {
54 | // The inner members of the type if an object
55 | const innerMap = get(type, 'symbol.members');
56 |
57 | // Determine the name of the primary key column joining the tables
58 | const foreignKeyMap = get(
59 | innerMap && innerMap.get('foreignKey'),
60 | 'valueDeclaration.initializer.symbol.members',
61 | );
62 |
63 | // Defaults to ${modelName}Id
64 | const primaryKeyName =
65 | get(
66 | foreignKeyMap && foreignKeyMap.get('name'),
67 | 'valueDeclaration.initializer.text',
68 | ) || `${key}Id`;
69 |
70 | // If the association is defined with a string, then the model name is the same as the key
71 | if (typeof type.value === 'string') {
72 | results[key] = { modelName: key, primaryKeyName };
73 | } else {
74 | const modelName = getValueFromObject(innerMap, 'modelName');
75 | const throughModelName = getValueFromObject(innerMap, 'throughModelName');
76 | results[key] =
77 | typeof modelName === 'string'
78 | ? { modelName, primaryKeyName, throughModelName }
79 | : { modelName: key, primaryKeyName, throughModelName };
80 | }
81 | });
82 |
83 | return results;
84 | }
85 |
--------------------------------------------------------------------------------
/src/checks/columnType.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | // external
3 | import { ModelAttributeColumnOptions, QueryTypes } from 'sequelize';
4 |
5 | // global
6 | import WildebeestDb from '@wildebeest/classes/WildebeestDb';
7 | import { ModelMap, SyncError } from '@wildebeest/types';
8 |
9 | /**
10 | * Mapping from sequelize type to data type
11 | */
12 | export const SEQUELIZE_TO_DATA_TYPE: { [k in string]: string } = {
13 | BLOB: 'bytea',
14 | BOOLEAN: 'boolean',
15 | DATE: 'timestamp with time zone',
16 | ENUM: 'USER-DEFINED',
17 | FLOAT: 'double precision',
18 | INTEGER: 'integer',
19 | STRING: 'character varying',
20 | JSONB: 'jsonb',
21 | TEXT: 'text',
22 | UUID: 'uuid',
23 | };
24 |
25 | /**
26 | * Determine the type of a column in the db
27 | *
28 | * @param db - The database to operate on
29 | * @param tableName - The name of the table
30 | * @param columnName - The name of the column
31 | * @returns The type of the column in postgres
32 | */
33 | export async function getColumnType(
34 | db: WildebeestDb,
35 | tableName: string,
36 | columnName: string,
37 | ): Promise {
38 | // Get the type for the column
39 | const [{ data_type }] = await db.queryInterface.sequelize.query(
40 | `
41 | SELECT column_name, data_type
42 | FROM information_schema.columns
43 | WHERE table_name = '${tableName}' AND column_name = '${columnName}';
44 | `,
45 | {
46 | type: QueryTypes.SELECT,
47 | },
48 | );
49 |
50 | return data_type;
51 | }
52 |
53 | /**
54 | * Check that the type of a column matches postgres
55 | *
56 | * @param db - The db to test on
57 | * @param tableName - The name of the table to operate on
58 | * @param name - The name of the column
59 | * @param definition - The model definition
60 | * @returns Any errors related to the type of the column
61 | */
62 | export default async function checkColumnType(
63 | db: WildebeestDb,
64 | tableName: string,
65 | name: string,
66 | definition: ModelAttributeColumnOptions,
67 | ): Promise {
68 | // Keep track of errors
69 | const errors: SyncError[] = [];
70 |
71 | // Get the current type
72 | const currentType = await getColumnType(db, tableName, name);
73 |
74 | // Get the expected type
75 | const expectedConstructorName = definition.type.constructor.name;
76 | const expectedType =
77 | SEQUELIZE_TO_DATA_TYPE[expectedConstructorName] || expectedConstructorName;
78 |
79 | // Check if type is correct
80 | if (expectedType !== currentType) {
81 | errors.push({
82 | message: `Unexpected type for column: "${name}" on table "${tableName}". Got "${currentType}" expected "${expectedType}"`, // eslint-disable-line max-len
83 | tableName,
84 | });
85 | }
86 |
87 | return errors;
88 | }
89 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@transcend-io/wildebeest",
3 | "version": "4.1.3",
4 | "description": "Type-safe sequelize with a simplified migration framework",
5 | "main": "build/src/index.js",
6 | "homepage": "https://github.com/transcend-io/wildebeest#readme",
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/transcend-io/wildebeest.git"
10 | },
11 | "author": "Transcend Privacy Inc.",
12 | "license": "MIT",
13 | "bugs": "https://github.com/transcend-io/wildebeest/issues",
14 | "files": [
15 | "build/**/*",
16 | "src/**/*",
17 | "tsconfig.json"
18 | ],
19 | "engines": {
20 | "npm": ">=6.4.1",
21 | "node": ">=10.15.0"
22 | },
23 | "bin": {
24 | "reorder-migrations": "./build/scripts/reorder_migrations.js"
25 | },
26 | "moduleSystem": "typescript",
27 | "private": false,
28 | "scripts": {
29 | "####### Linting #######": "",
30 | "lint": "eslint . --cache --ext .js,.ts,.tsx,.jsx",
31 | "lint:fix": "eslint . --cache --fix --ext .js,.ts,.tsx,.jsx",
32 | "ts:lint": "tslint ./src/**/*.ts",
33 | "####### Testing #######": "npm run build && npm run test:nobuild",
34 | "test": "echo no tests",
35 | "test:nobuild": "NODE_ENV=test ./node_modules/.bin/mocha \"build/**/*.test.{js,ts}\" --reporter spec --timeout 10000",
36 | "####### Build #######": "",
37 | "prepare": "npm run build",
38 | "build": "tsc && copy './generateMixins/{**/*.hbs,**/*.json}' build/generateMixins && copy './src/{**/*.hbs,**/*.json}' build/src && node ./fix_absolute_imports.js",
39 | "build:watch": "tsc --watch"
40 | },
41 | "peerDependencies": {
42 | "sequelize": "^5.21.6"
43 | },
44 | "dependencies": {
45 | "aws-sdk": "^2.690.0",
46 | "bluebird": "=3.7.2",
47 | "change-case": "4.1.1",
48 | "express": "^4.17.1",
49 | "handlebars": "^4.7.6",
50 | "lodash": "^4.17.15",
51 | "pluralize": "^8.0.0",
52 | "umzug": "^3.0.0-beta.5"
53 | },
54 | "devDependencies": {
55 | "@types/express": "^4.17.4",
56 | "@types/lodash": "^4.14.155",
57 | "@types/mocha": "^7.0.2",
58 | "@types/node": "^14.0.11",
59 | "@types/pluralize": "0.0.29",
60 | "@types/umzug": "^2.2.3",
61 | "@types/uuid": "^8.0.0",
62 | "@typescript-eslint/eslint-plugin": "^3.1.0",
63 | "@typescript-eslint/parser": "^3.1.0",
64 | "babel-eslint": "^10.1.0",
65 | "copy": "^0.3.2",
66 | "eslint": "^7.1.0",
67 | "eslint-config-airbnb-base": "^14.1.0",
68 | "eslint-config-prettier": "^6.10.1",
69 | "eslint-import-resolver-typescript": "^2.0.0",
70 | "eslint-plugin-import": "^2.20.2",
71 | "eslint-plugin-prettier": "^3.1.2",
72 | "mocha": "^7.2.0",
73 | "prettier": "^2.0.4",
74 | "sequelize": "5.21.12",
75 | "tslint": "^6.1.1",
76 | "tslint-microsoft-contrib": "^6.2.0",
77 | "typescript": "3.9.5"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/checks/joinBelongsTo.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import WildebeestDb from '@wildebeest/classes/WildebeestDb';
3 | import {
4 | ConfiguredModelDefinition,
5 | ModelMap,
6 | StringKeys,
7 | SyncError,
8 | } from '@wildebeest/types';
9 |
10 | /**
11 | * Check that the associations are correct for a `belongsTo` between two tables that are joining
12 | *
13 | * @param db - The wildebeest db
14 | * @param model - The model to check the associations for
15 | * @returns Any errors related to the belongs to association config of a join table
16 | */
17 | export default async function checkJoinBelongsTo(
18 | db: WildebeestDb,
19 | { associations, tableName }: ConfiguredModelDefinition>,
20 | ): Promise {
21 | // Keep track of errors
22 | const errors: SyncError[] = [];
23 |
24 | // Ensure db is defined
25 | if (!db) {
26 | // TODO maybe have a type that has these enforced to be set to prevent need for type check all over
27 | throw new Error(
28 | `wildebeest instance must be initialized before checking belongsTo`,
29 | );
30 | }
31 |
32 | // The belongs to associations
33 | const { belongsTo } = associations;
34 |
35 | // Join tables must have belongsToMany specified by each of its belongsTo
36 | // Ensure that there are two belongsTo association
37 | if (Object.values(belongsTo).length < 2) {
38 | errors.push({
39 | message: `Join table: "${tableName}" expected to have at least 2 belongsTo associations`,
40 | tableName,
41 | });
42 | } else {
43 | // Joins between these models
44 | // TODO expecting to be first two there
45 | const [first, second] = Object.keys(belongsTo) as Extract<
46 | keyof TModels,
47 | string
48 | >[];
49 |
50 | // Helper to check if belongsToMany config is setup
51 | const hasBelongsToConfig = (
52 | firstModelName: StringKeys,
53 | secondModelName: StringKeys,
54 | ): boolean => {
55 | const oppositeAssociations = db.model(firstModelName).definition
56 | .associations;
57 | return (
58 | !!oppositeAssociations &&
59 | !!oppositeAssociations.belongsToMany &&
60 | secondModelName in oppositeAssociations.belongsToMany
61 | );
62 | };
63 |
64 | // Check if the first table is setup
65 | const firstValid = hasBelongsToConfig(first, second);
66 | if (!firstValid) {
67 | errors.push({
68 | message: `Table "${first}" missing belongsToMany "${second}"`,
69 | tableName,
70 | });
71 | }
72 |
73 | // Check if the second table is setup
74 | const secondValid = hasBelongsToConfig(second, first);
75 | if (!secondValid) {
76 | errors.push({
77 | message: `Table "${second}" missing belongsToMany "${first}"`,
78 | tableName,
79 | });
80 | }
81 | }
82 | return errors;
83 | }
84 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | extends: [
4 | 'airbnb-base',
5 | 'plugin:@typescript-eslint/recommended',
6 | 'prettier',
7 | 'prettier/@typescript-eslint',
8 | 'plugin:prettier/recommended',
9 | // "@transcend-io/transcend",
10 | ],
11 | env: {
12 | browser: true,
13 | node: true,
14 | es6: true,
15 | mocha: true,
16 | },
17 | plugins: ['import', '@typescript-eslint'],
18 | parserOptions: {
19 | ecmaVersion: 6,
20 | sourceType: 'module',
21 | ecmaFeatures: {
22 | jsx: true,
23 | },
24 | },
25 | rules: {
26 | '@typescript-eslint/prefer-interface': 0,
27 | '@typescript-eslint/indent': 0,
28 | '@typescript-eslint/explicit-function-return-type': [
29 | 'warn',
30 | { allowExpressions: true },
31 | ],
32 | '@typescript-eslint/camelcase': 0,
33 | '@typescript-eslint/interface-name-prefix': 0,
34 | '@typescript-eslint/no-var-requires': 0,
35 | 'template-curly-spacing': 0,
36 | '@typescript-eslint/no-use-before-define': 0,
37 | 'arrow-parens': ['error', 'always'],
38 | 'arrow-body-style': [2, 'as-needed'],
39 | 'class-methods-use-this': ['error'],
40 | 'comma-dangle': 0, // [2, "always-multiline"] handled by prettier
41 | 'function-paren-newline': 0,
42 | 'import/imports-first': ['error'],
43 | 'import/extensions': 0,
44 | 'import/newline-after-import': ['error'],
45 | 'import/no-dynamic-require': ['error'],
46 | 'import/no-extraneous-dependencies': 0,
47 | 'import/no-named-as-default': 0,
48 | 'import/order': 0,
49 | // "import/no-relative-parent-imports": ["warn"],
50 | 'import/no-unresolved': 2,
51 | 'import/no-webpack-loader-syntax': ['error'],
52 | 'import/prefer-default-export': 0,
53 | indent: 0,
54 | // "jsdoc/check-types": ["error"],
55 | 'max-len': ['error', 125, { comments: 200 }],
56 | 'max-lines': ['error', 350],
57 | // "newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }],
58 | 'no-confusing-arrow': 0,
59 | 'no-console': 0,
60 | 'no-underscore-dangle': 0,
61 | 'no-multi-spaces': ['error'],
62 | 'no-use-before-define': ['error'],
63 | // "object-curly-newline": ["error", { "multiline": true, "minProperties": 4, "consistent": true }],
64 | 'object-property-newline': 'error',
65 | 'prefer-template': 2,
66 | 'require-jsdoc': [
67 | 'error',
68 | {
69 | require: {
70 | FunctionDeclaration: true,
71 | MethodDefinition: true,
72 | ClassDeclaration: true,
73 | ArrowFunctionExpression: false,
74 | FunctionExpression: true,
75 | },
76 | },
77 | ],
78 | 'require-yield': ['error'],
79 | 'sort-vars': ['error', { ignoreCase: true }],
80 | },
81 | settings: {
82 | 'import/resolver': {
83 | typescript: {},
84 | },
85 | },
86 | };
87 |
--------------------------------------------------------------------------------
/src/checks/associationConfig.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | // external
3 | import { QueryTypes } from 'sequelize';
4 |
5 | // global
6 | import { ConfiguredAttribute, ModelMap, SyncError } from '@wildebeest/types';
7 | import getForeignKeyConfig from '@wildebeest/utils/getForeignKeyConfig';
8 |
9 | // local
10 | import Wildebeest from '@wildebeest/classes/Wildebeest';
11 | import WildebeestDb from '@wildebeest/classes/WildebeestDb';
12 |
13 | /**
14 | * Check if the database has a constraint
15 | *
16 | * @param db - The db instance
17 | * @param name - The name of the constraint
18 | * @returns True if the constraint is defined
19 | */
20 | export async function hasConstraint(
21 | db: WildebeestDb,
22 | name: string,
23 | ): Promise {
24 | const [constraint] = await db.queryInterface.sequelize.query(
25 | `
26 | SELECT conname FROM pg_constraint WHERE conname='${name}'
27 | `,
28 | {
29 | type: QueryTypes.SELECT,
30 | },
31 | );
32 | return !!constraint;
33 | }
34 |
35 | /**
36 | * Ensure the constraint between the table column associations is valid
37 | *
38 | * @param wildebeest - The wildebeest configuration
39 | * @param tableName - The name of the table to check
40 | * @param name - The name of the column
41 | * @param definition - The attribute definition
42 | * @returns Any errors with the association configuration
43 | */
44 | export default async function checkAssociationConfig(
45 | { namingConventions, db }: Wildebeest,
46 | tableName: string,
47 | name: string,
48 | definition: ConfiguredAttribute,
49 | ): Promise {
50 | // Keep track of errors
51 | const errors: SyncError[] = [];
52 |
53 | // The name of the constraint
54 | const constraintName = namingConventions.foreignKeyConstraint(
55 | tableName,
56 | name,
57 | );
58 |
59 | // Ensure that the constraint exists
60 | const constraintExists = await hasConstraint(db, constraintName);
61 | if (!constraintExists) {
62 | errors.push({
63 | message: `Missing foreign key constraint for "${name}" on table "${tableName}": "${constraintName}"`,
64 | tableName,
65 | });
66 | } else {
67 | // Ensure the constraint has proper onDelete
68 | const { delete_rule } = await getForeignKeyConfig(db, constraintName);
69 | const expected =
70 | definition.associationOptions && definition.associationOptions.onDelete
71 | ? definition.associationOptions.onDelete.toUpperCase()
72 | : 'NO ACTION';
73 |
74 | if (delete_rule !== expected) {
75 | errors.push({
76 | message: `Invalid foreign key onDelete for column "${name}" of table "${tableName}". Got "${delete_rule}" expected "${expected}"`, // eslint-disable-line max-len
77 | tableName,
78 | });
79 | }
80 | }
81 |
82 | return errors;
83 | } /* eslint-enable camelcase */
84 |
--------------------------------------------------------------------------------
/src/migrationTypes/renameConstraint.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import {
3 | MigrationDefinition,
4 | MigrationTransactionOptions,
5 | ModelMap,
6 | } from '@wildebeest/types';
7 | import listConstraintNames from '@wildebeest/utils/listConstraintNames';
8 |
9 | // classes
10 | import WildebeestDb from '@wildebeest/classes/WildebeestDb';
11 |
12 | /**
13 | * Options for changing the name of a constraint
14 | */
15 | export type RenameConstraintOptions = {
16 | /** The name of the table to rename the constraint on */
17 | tableName: string;
18 | /** The old name of the constraint */
19 | oldName: string;
20 | /** The new name of the constraint */
21 | newName: string;
22 | /** If the oldName does not exist, simply skip this transformation (for backwards compatibility of migrations) */
23 | skipIfExists?: boolean;
24 | };
25 |
26 | /**
27 | * Change the name of a constraint
28 | *
29 | * @param db - The database to migrate against
30 | * @param options - The options to use to rename from
31 | * @param rawTransactionOptions - The previous transaction options
32 | * @returns The change constraint promise
33 | */
34 | export async function changeConstraintName(
35 | options: RenameConstraintOptions,
36 | transactionOptions: MigrationTransactionOptions,
37 | db: WildebeestDb,
38 | ): Promise {
39 | // Raw query interface
40 | const { queryT } = transactionOptions;
41 | const { tableName, oldName, newName, skipIfExists } = options;
42 |
43 | // If allowed to skip and constraint exists, skip it
44 | if (skipIfExists) {
45 | const constraintNames = await listConstraintNames(
46 | db,
47 | tableName,
48 | transactionOptions,
49 | );
50 | if (constraintNames.includes(newName)) {
51 | return;
52 | }
53 | }
54 |
55 | // Rename the constraint
56 | await queryT.raw(
57 | `
58 | ALTER TABLE "${tableName}" RENAME CONSTRAINT "${oldName}" TO "${newName}";
59 | `,
60 | );
61 | }
62 |
63 | /**
64 | * Change the name of a constraint
65 | *
66 | * @param options - Options for renaming a constraint
67 | * @returns The rename constraint migrator
68 | */
69 | export default function renameConstraint(
70 | options: RenameConstraintOptions,
71 | ): MigrationDefinition {
72 | const { oldName, newName, ...rest } = options;
73 | return {
74 | up: async (wildebeest, withTransaction) =>
75 | withTransaction((transactionOptions) =>
76 | changeConstraintName(
77 | { oldName, newName, ...rest },
78 | transactionOptions,
79 | wildebeest.db,
80 | ),
81 | ),
82 | down: async (wildebeest, withTransaction) =>
83 | withTransaction((transactionOptions) =>
84 | changeConstraintName(
85 | { newName: oldName, oldName: newName, ...rest },
86 | transactionOptions,
87 | wildebeest.db,
88 | ),
89 | ),
90 | };
91 | }
92 |
--------------------------------------------------------------------------------
/src/checks/columnDefinition.ts:
--------------------------------------------------------------------------------
1 | // global
2 | import Wildebeest from '@wildebeest/classes/Wildebeest';
3 | import {
4 | ConfiguredModelDefinition,
5 | ModelMap,
6 | StringKeys,
7 | SyncError,
8 | } from '@wildebeest/types';
9 | import isEnum from '@wildebeest/utils/isEnum';
10 |
11 | // local
12 | import checkAllowNullConstraint from './allowNullConstraint';
13 | import checkAssociationConfig from './associationConfig';
14 | import checkColumnType from './columnType';
15 | import checkDefaultValue from './defaultValue';
16 | import checkEnumDefinition from './enumDefinition';
17 | import checkPrimaryKeyDefinition from './primaryKeyDefinition';
18 | import checkUniqueConstraint from './uniqueConstraint';
19 |
20 | /**
21 | * Check that a db model column definition defined by sequelize is matching the postgres db
22 | *
23 | * @param wildebeest - The wildebeest configuration
24 | * @param model - The model to check
25 | * @param name - The name of the column to check
26 | * @returns Any errors related to the column definition
27 | */
28 | export default async function checkColumnDefinition(
29 | wildebeest: Wildebeest,
30 | {
31 | tableName,
32 | attributes,
33 | rawAttributes,
34 | }: ConfiguredModelDefinition>,
35 | name: string,
36 | ): Promise {
37 | // Keep track of errors
38 | const errors: SyncError[] = [];
39 |
40 | // Get the column definition
41 | const definition = attributes[name];
42 | const rawDefinition = rawAttributes[name];
43 | if (!definition) {
44 | errors.push({
45 | message: `Missing attribute definition for column "${name}" in table "${tableName}"`,
46 | tableName,
47 | });
48 | return errors;
49 | }
50 |
51 | // Run the column checks
52 | const allErrors = await Promise.all([
53 | // If the definition is an enum, verify that the enum configuration is correct
54 | !isEnum(definition)
55 | ? []
56 | : checkEnumDefinition(wildebeest, tableName, name, definition),
57 | // If the column is a primary key, ensure its constraint exists
58 | !definition.primaryKey
59 | ? []
60 | : checkPrimaryKeyDefinition(wildebeest, tableName, name),
61 | // Ensure the association is configured properly
62 | !definition.isAssociation
63 | ? []
64 | : checkAssociationConfig(wildebeest, tableName, name, definition),
65 | // Ensure the unique constraint is set properly
66 | rawDefinition
67 | ? checkUniqueConstraint(wildebeest, tableName, name, rawDefinition)
68 | : [],
69 | // Ensure allowNull is set properly
70 | checkAllowNullConstraint(wildebeest.db, tableName, name, definition),
71 | // Ensure the default value is correct
72 | checkDefaultValue(wildebeest, tableName, name, definition),
73 | // Ensure the type is set properly,
74 | checkColumnType(wildebeest.db, tableName, name, definition),
75 | ]);
76 |
77 | return errors.concat(...allErrors);
78 | }
79 |
--------------------------------------------------------------------------------