@axinom/mosaic-cli 0.14.2-rc.8 → 0.15.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -5
- package/src/cli/README.md +60 -0
- package/src/cli/index.ts +47 -0
- package/src/commands/apply-templates/apply-templates.spec.ts +623 -0
- package/src/commands/apply-templates/apply-templates.ts +494 -0
- package/src/commands/apply-templates/bitwarden-vault.ts +130 -0
- package/src/commands/apply-templates/index.ts +1 -0
- package/src/commands/create-extension-config/create-extension-config.ts +92 -0
- package/src/commands/create-extension-config/index.ts +23 -0
- package/src/commands/get-access-token/get-access-token-options.ts +9 -0
- package/src/commands/get-access-token/get-dev-access-token.ts +32 -0
- package/src/commands/get-access-token/index.ts +66 -0
- package/src/commands/graphql-diff.ts +143 -0
- package/src/commands/msg-codegen/codegen.ts +891 -0
- package/src/commands/msg-codegen/index.ts +48 -0
- package/src/commands/msg-codegen/lint.ts +84 -0
- package/src/commands/msg-codegen/message-codegen-options.ts +7 -0
- package/src/commands/msg-diff/asyncapi-override.ts +31 -0
- package/src/commands/msg-diff/git-checkout-tmp.ts +73 -0
- package/src/commands/msg-diff/index.ts +53 -0
- package/src/commands/msg-diff/message-diff-options.ts +7 -0
- package/src/commands/msg-diff/msg-diff.spec.ts +412 -0
- package/src/commands/msg-diff/msg-diff.ts +364 -0
- package/src/commands/msg-diff/test-resources/0/1-asyncapi.yml +38 -0
- package/src/commands/msg-diff/test-resources/0/2-asyncapi.yml +36 -0
- package/src/commands/msg-diff/test-resources/0/command.json +74 -0
- package/src/commands/msg-diff/test-resources/0/event.json +25 -0
- package/src/commands/msg-diff/test-resources/1/1-asyncapi.yml +25 -0
- package/src/commands/msg-diff/test-resources/1/moved-event.json +25 -0
- package/src/commands/msg-diff/test-resources/common.json +20 -0
- package/src/commands/pg-dump/README.md +21 -0
- package/src/commands/pg-dump/generate.ts +146 -0
- package/src/commands/pg-dump/index.ts +39 -0
- package/src/commands/pg-dump/pg-dump-options.ts +6 -0
- package/src/commands/publish-schema-to-db/README.md +130 -0
- package/src/commands/publish-schema-to-db/abstractions/base-smart-tags.ts +6 -0
- package/src/commands/publish-schema-to-db/abstractions/index.ts +5 -0
- package/src/commands/publish-schema-to-db/abstractions/pg-column.ts +31 -0
- package/src/commands/publish-schema-to-db/abstractions/pg-fk-column.ts +6 -0
- package/src/commands/publish-schema-to-db/abstractions/pg-table.ts +55 -0
- package/src/commands/publish-schema-to-db/abstractions/pg-type.ts +8 -0
- package/src/commands/publish-schema-to-db/content-entity-model.ts +93 -0
- package/src/commands/publish-schema-to-db/generate.ts +82 -0
- package/src/commands/publish-schema-to-db/index.ts +49 -0
- package/src/commands/publish-schema-to-db/jest.config.js +9 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/fk-column.spec.ts +42 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/fk-column.ts +41 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/index.ts +4 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/pk-column.spec.ts +47 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/pk-column.ts +34 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/primitive-column.spec.ts +65 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/primitive-column.ts +62 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/virtual-fk-column.spec.ts +24 -0
- package/src/commands/publish-schema-to-db/pg-models/columns/virtual-fk-column.ts +34 -0
- package/src/commands/publish-schema-to-db/pg-models/json-schema-parse-utils.spec.ts +182 -0
- package/src/commands/publish-schema-to-db/pg-models/json-schema-parse-utils.ts +166 -0
- package/src/commands/publish-schema-to-db/pg-models/pg-sql-gen-utils.spec.ts +19 -0
- package/src/commands/publish-schema-to-db/pg-models/pg-sql-gen-utils.ts +237 -0
- package/src/commands/publish-schema-to-db/pg-models/pgl-utils.spec.ts +19 -0
- package/src/commands/publish-schema-to-db/pg-models/pgl-utils.ts +115 -0
- package/src/commands/publish-schema-to-db/pg-models/tables/content-entity-table.ts +104 -0
- package/src/commands/publish-schema-to-db/pg-models/tables/index.ts +3 -0
- package/src/commands/publish-schema-to-db/pg-models/tables/object-property-table.ts +113 -0
- package/src/commands/publish-schema-to-db/pg-models/tables/relations-table.ts +115 -0
- package/src/commands/publish-schema-to-db/postprocessors/collection-postprocessor.ts +33 -0
- package/src/commands/publish-schema-to-db/postprocessors/content-entity-model-postprocessor.ts +13 -0
- package/src/commands/publish-schema-to-db/postprocessors/episode-postprocessor.ts +37 -0
- package/src/commands/publish-schema-to-db/postprocessors/index.ts +6 -0
- package/src/commands/publish-schema-to-db/postprocessors/movie-postprocessor.ts +30 -0
- package/src/commands/publish-schema-to-db/postprocessors/postprocessing-utils.ts +21 -0
- package/src/commands/publish-schema-to-db/postprocessors/season-postprocessor.ts +37 -0
- package/src/commands/publish-schema-to-db/postprocessors/tvshow-postprocessor.ts +30 -0
- package/src/commands/publish-schema-to-db/publish-schema-to-db-options.ts +15 -0
- package/src/commands/publish-schema-to-db/types/sql-formatter.d.ts +10 -0
- package/src/exports.ts +2 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertError,
|
|
3
|
+
getBasicDbConfigDefinitions,
|
|
4
|
+
getValidatedConfig,
|
|
5
|
+
isNullOrWhitespace,
|
|
6
|
+
pick,
|
|
7
|
+
} from '@axinom/mosaic-service-common';
|
|
8
|
+
import * as chalk from 'chalk';
|
|
9
|
+
import { exec, ExecException } from 'child_process';
|
|
10
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
11
|
+
import { dirname, join, normalize, relative } from 'path';
|
|
12
|
+
import { PgDumpOptions } from './pg-dump-options';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Calculates a path to `docker-compose.yaml` file based on initial path.
|
|
16
|
+
* For example, if `scripts/infra` is provided as initial path, it will look for `scripts/infra/docker-compose.yml` in current folder, then in folder one hierarchy level up, traversing directories towards root.
|
|
17
|
+
* Returns a value like `../../../scripts/infra` or throws an error.
|
|
18
|
+
* Eliminates the need to calculate how many `..` you must have in the path to docker compose file which is passed as a cli parameter.
|
|
19
|
+
*/
|
|
20
|
+
const findComposePath = (initialPath: string): string => {
|
|
21
|
+
let startPath = process.cwd();
|
|
22
|
+
let lastPath = '';
|
|
23
|
+
|
|
24
|
+
while (lastPath !== startPath) {
|
|
25
|
+
const lookupPath = join(startPath, initialPath, 'docker-compose.yml');
|
|
26
|
+
if (existsSync(lookupPath)) {
|
|
27
|
+
return dirname(relative(process.cwd(), lookupPath));
|
|
28
|
+
} else {
|
|
29
|
+
// Preserve currently checked path for while condition comparison
|
|
30
|
+
lastPath = startPath;
|
|
31
|
+
// Remove last segment from path, move to directory closer to root
|
|
32
|
+
startPath = normalize(join(startPath, '..'));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
throw Error(
|
|
36
|
+
`'${initialPath}/docker-compose.yml' file not found. Please make sure that correct composeFolderPath parameter is provided.`,
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Loads and validates all required environment variables that are used to create a shadow database connection string.
|
|
42
|
+
* For this to work, CLI script must be called with pre-loaded variables. e.g.
|
|
43
|
+
* - using dotenv-cli script would look like this: `dotenv -- mosaic pg-dump -c scripts/infra`
|
|
44
|
+
* - using env-cmd script could look like this: `env-cmd -f .env env-cmd -f ../../../.env mosaic pg-dump -c scripts/infra`
|
|
45
|
+
*/
|
|
46
|
+
const getDumpDbConnectionString = (connectionString?: string): string => {
|
|
47
|
+
if (!isNullOrWhitespace(connectionString)) {
|
|
48
|
+
return connectionString;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const configDefinitions = pick(
|
|
53
|
+
getBasicDbConfigDefinitions(),
|
|
54
|
+
'pgHost',
|
|
55
|
+
'pgPort',
|
|
56
|
+
'dbName',
|
|
57
|
+
'dbOwner',
|
|
58
|
+
'dbOwnerPassword',
|
|
59
|
+
'dbOwnerConnectionString',
|
|
60
|
+
'dbShadowConnectionString',
|
|
61
|
+
'pgUserSuffix',
|
|
62
|
+
);
|
|
63
|
+
const config = getValidatedConfig(configDefinitions);
|
|
64
|
+
return config.dbShadowConnectionString;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
assertError(error);
|
|
67
|
+
throw new Error(
|
|
68
|
+
`${error.message}\n\nSpecified environment variables must be pre-loaded, or a database connection string can be specified explicitly using 'connectionString' cli parameter.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Creates a dump directory (if not existing already), where resulting sql dump file will be created/updated.
|
|
75
|
+
* Returns absolute path to dump file, including filename.
|
|
76
|
+
*/
|
|
77
|
+
const ensureDumpDirectoryExist = (targetPath: string): string => {
|
|
78
|
+
const dbSchemaExportPath = join(process.cwd(), targetPath);
|
|
79
|
+
const dirPath = dirname(dbSchemaExportPath);
|
|
80
|
+
if (!existsSync(dirPath)) {
|
|
81
|
+
mkdirSync(dirPath, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
return dbSchemaExportPath;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returns a readable timestamp in current locale time, e.g. `[2021-05-13 13:02:12.685]`
|
|
88
|
+
*/
|
|
89
|
+
const getTimestamp = (): string => {
|
|
90
|
+
const offset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds
|
|
91
|
+
const localISOTime = new Date(Date.now() - offset)
|
|
92
|
+
.toISOString()
|
|
93
|
+
.slice(0, -1)
|
|
94
|
+
.replace('T', ' ');
|
|
95
|
+
return `[${localISOTime}]`;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const generate = (options: PgDumpOptions): void => {
|
|
99
|
+
try {
|
|
100
|
+
console.log(getTimestamp(), `Starting pg-dump command...`);
|
|
101
|
+
const composePath = findComposePath(options.composeFolderPath);
|
|
102
|
+
const connectionString = getDumpDbConnectionString(
|
|
103
|
+
options.connectionString,
|
|
104
|
+
);
|
|
105
|
+
const dumpPath = ensureDumpDirectoryExist(options.dumpPath);
|
|
106
|
+
const excludeSchemas = isNullOrWhitespace(options.excludeSchemas)
|
|
107
|
+
? []
|
|
108
|
+
: options.excludeSchemas
|
|
109
|
+
.split(',')
|
|
110
|
+
.map((schemaName) => `--exclude-schema=${schemaName}`);
|
|
111
|
+
const dumpOptions = [
|
|
112
|
+
'--no-sync',
|
|
113
|
+
'--schema-only',
|
|
114
|
+
'--no-owner',
|
|
115
|
+
...excludeSchemas,
|
|
116
|
+
connectionString,
|
|
117
|
+
].join(' ');
|
|
118
|
+
|
|
119
|
+
exec(
|
|
120
|
+
`cd ${composePath} && docker-compose exec -T postgres pg_dump ${dumpOptions} > ${dumpPath}`,
|
|
121
|
+
(_error: ExecException | null, stdout: string, stderr: string) => {
|
|
122
|
+
if (stdout) {
|
|
123
|
+
console.log(getTimestamp(), 'pg_dump info:', stdout);
|
|
124
|
+
}
|
|
125
|
+
if (stderr) {
|
|
126
|
+
console.log(
|
|
127
|
+
getTimestamp(),
|
|
128
|
+
chalk.red('Command failed:'),
|
|
129
|
+
stderr.trim(),
|
|
130
|
+
);
|
|
131
|
+
} else {
|
|
132
|
+
console.log(
|
|
133
|
+
getTimestamp(),
|
|
134
|
+
chalk.green('Success:'),
|
|
135
|
+
`Database schema dump created at '${chalk.blueBright(
|
|
136
|
+
options.dumpPath,
|
|
137
|
+
)}'`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
assertError(err);
|
|
144
|
+
console.log(getTimestamp(), chalk.red('Command failed:'), err.message);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { CommandModule } from 'yargs';
|
|
2
|
+
import { generate } from './generate';
|
|
3
|
+
import { PgDumpOptions } from './pg-dump-options';
|
|
4
|
+
|
|
5
|
+
export const pgDump: CommandModule<unknown, PgDumpOptions> = {
|
|
6
|
+
command: 'pg-dump',
|
|
7
|
+
describe:
|
|
8
|
+
'Development command that uses a default PostgreSQL pg_dump utility to create a schema dump file of a specific database based on an existing docker-compose.yml file.',
|
|
9
|
+
builder: (yargs) =>
|
|
10
|
+
yargs
|
|
11
|
+
.option('composeFolderPath', {
|
|
12
|
+
alias: 'c',
|
|
13
|
+
default: 'infra',
|
|
14
|
+
describe:
|
|
15
|
+
'Relative path to docker-compose.yml file, e.g. `infra` or `scripts/infra`. Script will look for such directory starting from current project, traversing directories closer to root, so no need to specify a path like `../../../infra`',
|
|
16
|
+
string: true,
|
|
17
|
+
})
|
|
18
|
+
.option('dumpPath', {
|
|
19
|
+
alias: 'd',
|
|
20
|
+
default: 'src/generated/db/schema.sql',
|
|
21
|
+
describe:
|
|
22
|
+
'Relative path from working directory to a schema SQL file which will be created by pg_dump utility, e.g. `src/generated/db/schema.sql`',
|
|
23
|
+
string: true,
|
|
24
|
+
})
|
|
25
|
+
.option('excludeSchemas', {
|
|
26
|
+
alias: 'e',
|
|
27
|
+
default: 'graphile_migrate,graphile_worker',
|
|
28
|
+
describe:
|
|
29
|
+
'A comma-separated string of database schema names that should be excluded from pg_dump, e.g. `schema_one,schema_two`',
|
|
30
|
+
string: true,
|
|
31
|
+
})
|
|
32
|
+
.option('connectionString', {
|
|
33
|
+
alias: 's',
|
|
34
|
+
describe:
|
|
35
|
+
'The connection string to the database to be used to take a schema dump. Can be omitted if database-related environment variables are preloaded',
|
|
36
|
+
string: true,
|
|
37
|
+
}),
|
|
38
|
+
handler: generate,
|
|
39
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Overview
|
|
2
|
+
|
|
3
|
+
This a CLI tool for bootstrapping the development process of a catalog-like
|
|
4
|
+
service. It generates a Postgres database schema together with corresponding
|
|
5
|
+
PostGraphile Smart Tags from an array of content type publish format schemas.
|
|
6
|
+
|
|
7
|
+
This tool will output two files:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
${YOUR_PROJECT}
|
|
11
|
+
├── migrations
|
|
12
|
+
│ └── current.sql
|
|
13
|
+
└── src
|
|
14
|
+
└── plugins
|
|
15
|
+
└── smart-tags-plugin.ts
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**NOTE:** These files will be overwritten each time you run this tool.
|
|
19
|
+
|
|
20
|
+
# Usage
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
yarn mosaic publish-schema-to-db -i '/path/to/schemas/**/*-published-event.json' -o '/path/to/your/service/root'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
# Assumptions and terminology
|
|
29
|
+
|
|
30
|
+
**Content metadata schema** - JSON schema describing the publish format for
|
|
31
|
+
content metadata (movie, collection, TV show etc.).
|
|
32
|
+
|
|
33
|
+
When mapping the publish message JSON schema to a Postgres schema the following
|
|
34
|
+
conventions are used (this applies for each defined content type).
|
|
35
|
+
|
|
36
|
+
**Content metadata schema** is decomposed into:
|
|
37
|
+
|
|
38
|
+
- **Content entity** - root entity of content metadata.
|
|
39
|
+
- An array of **primitive properties**.
|
|
40
|
+
- An array of **object properties**.
|
|
41
|
+
- A _relations_ table linking the content to other **content entities** (e.g.
|
|
42
|
+
related items, genres).
|
|
43
|
+
|
|
44
|
+
## Customizing output
|
|
45
|
+
|
|
46
|
+
In addition to the general assumptions described above it is also possible to
|
|
47
|
+
customize schema generation per content type via postprocessors, see
|
|
48
|
+
`src/postprocessors`.
|
|
49
|
+
|
|
50
|
+
## Limitations
|
|
51
|
+
|
|
52
|
+
This tool assumes at most one level of nested objects in the input JSON schema.
|
|
53
|
+
|
|
54
|
+
SUPPORTED
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"$schema": "http://json-schema.org/draft-04/schema",
|
|
59
|
+
"properties": {
|
|
60
|
+
"object_prop": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"a": {
|
|
64
|
+
"type": "string"
|
|
65
|
+
},
|
|
66
|
+
"b": {
|
|
67
|
+
"type": "number"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
NOT SUPPORTED
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"$schema": "http://json-schema.org/draft-04/schema",
|
|
80
|
+
"properties": {
|
|
81
|
+
"object_prop": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {
|
|
84
|
+
"a": {
|
|
85
|
+
"type": "string"
|
|
86
|
+
},
|
|
87
|
+
"nested_object_prop": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"properties": {
|
|
90
|
+
"b": {
|
|
91
|
+
"type": "number"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Mapping of supported property types to DB constructs:
|
|
102
|
+
|
|
103
|
+
- primitive (string, int etc.) - primitive type field
|
|
104
|
+
- primitive array - array of primitive type
|
|
105
|
+
- object - 1-to-1
|
|
106
|
+
- object array - 1-to-many
|
|
107
|
+
|
|
108
|
+
# TODO
|
|
109
|
+
|
|
110
|
+
- Named indexes.
|
|
111
|
+
- Generate migrations.
|
|
112
|
+
- https://github.com/michaelsogos/pg-diff
|
|
113
|
+
- https://www.npmjs.com/package/pg-compare
|
|
114
|
+
- https://databaseci.com/docs/migra
|
|
115
|
+
- Test table generators.
|
|
116
|
+
- Better logging.
|
|
117
|
+
- Support nullable (required/not required)
|
|
118
|
+
- Improve property exclusion logic
|
|
119
|
+
|
|
120
|
+
Frank's comment:
|
|
121
|
+
|
|
122
|
+
> Checked a bit on the implementation. The "ignored properties" seems to be
|
|
123
|
+
> just a flat string list. So it is not possible to exclude propertyA from
|
|
124
|
+
> movie but keep propertyB on TV show - right? In the used env variable I see
|
|
125
|
+
> $schema,related_items. The schema part I understand (and I would even hard-code that potentially - or even exclude anything starting with $).
|
|
126
|
+
> But for "related_items" I am very unsure about. The name can be custom, it
|
|
127
|
+
> can change over time etc. I would rather say to hard code the \$ part. And
|
|
128
|
+
> then have some callback function that you can implement in code that would
|
|
129
|
+
> get the content type (and for objects also that type). And then this could
|
|
130
|
+
> be customized to exclude if it is needed.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { BaseSmartTags } from './base-smart-tags';
|
|
2
|
+
import { PgTable } from './pg-table';
|
|
3
|
+
import { PgType } from './pg-type';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Abstraction over a Postgres table column.
|
|
7
|
+
*/
|
|
8
|
+
export interface PgColumn {
|
|
9
|
+
/** Name of the column. */
|
|
10
|
+
name: string;
|
|
11
|
+
|
|
12
|
+
/** Optional display name to override the `name` in the generated GQL API. */
|
|
13
|
+
displayName?: string;
|
|
14
|
+
|
|
15
|
+
/** Description of the property. */
|
|
16
|
+
readonly description?: string;
|
|
17
|
+
|
|
18
|
+
/** Reference to the parent table that holds this column. */
|
|
19
|
+
readonly table: PgTable;
|
|
20
|
+
|
|
21
|
+
/** Postgres data type associated with this column. */
|
|
22
|
+
readonly type: PgType;
|
|
23
|
+
|
|
24
|
+
/** Expression defining the column. It will be used inside the CREATE TABLE statement. */
|
|
25
|
+
buildExpression(): string;
|
|
26
|
+
|
|
27
|
+
buildSmartTags(): BaseSmartTags;
|
|
28
|
+
|
|
29
|
+
/** Additional statements associated with the column e.g. indexes etc. */
|
|
30
|
+
buildAdditionalStatements(): string[];
|
|
31
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PgSmartTagTags } from 'graphile-utils';
|
|
2
|
+
import { BaseSmartTags } from './base-smart-tags';
|
|
3
|
+
import { PgColumn } from './pg-column';
|
|
4
|
+
import { PgFkColumn } from './pg-fk-column';
|
|
5
|
+
|
|
6
|
+
// TODO: Consider adding a separate property for FKs for consistency.
|
|
7
|
+
/**
|
|
8
|
+
* Abstraction over a Postgres table.
|
|
9
|
+
*/
|
|
10
|
+
export interface PgTable {
|
|
11
|
+
/** Name of the table. */
|
|
12
|
+
name: string;
|
|
13
|
+
|
|
14
|
+
/** Optional display name to override the `name` in the generated GQL API. */
|
|
15
|
+
displayName?: string;
|
|
16
|
+
|
|
17
|
+
/** Description of the table. */
|
|
18
|
+
readonly description?: string;
|
|
19
|
+
|
|
20
|
+
/** Table's PK */
|
|
21
|
+
readonly pk: PgColumn;
|
|
22
|
+
|
|
23
|
+
/** FKs to other tables. */
|
|
24
|
+
readonly fks: PgFkColumn[];
|
|
25
|
+
|
|
26
|
+
/** Virtual FKs to other tables (PostGraphile-specific). */
|
|
27
|
+
readonly virtualFks: PgFkColumn[];
|
|
28
|
+
|
|
29
|
+
/** Array of regular 'data' columns in the table. */
|
|
30
|
+
readonly columns: PgColumn[];
|
|
31
|
+
|
|
32
|
+
/** Builds a fully qualified table name - [schema].[table_name]. */
|
|
33
|
+
buildFullName(): string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds an (ordered) array of SQL statements required for creating a table.
|
|
37
|
+
*/
|
|
38
|
+
buildStatements(): string[];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Builds additional smart tags for PostGraphile.
|
|
42
|
+
*/
|
|
43
|
+
buildSmartTags(): TableSmartTags;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TableSmartTags {
|
|
47
|
+
tags?: PgSmartTagTags;
|
|
48
|
+
description?: string;
|
|
49
|
+
attribute?: {
|
|
50
|
+
[attributeName: string]: BaseSmartTags;
|
|
51
|
+
};
|
|
52
|
+
constraint?: {
|
|
53
|
+
[constraintName: string]: BaseSmartTags;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { JSONSchema4 } from 'json-schema';
|
|
3
|
+
import { PgTable, TableSmartTags } from './abstractions';
|
|
4
|
+
import { separateProperties } from './pg-models/json-schema-parse-utils';
|
|
5
|
+
import { isReservedPgWord } from './pg-models/pg-sql-gen-utils';
|
|
6
|
+
import { ContentEntityTable, ObjectPropertyTable } from './pg-models/tables';
|
|
7
|
+
import { PublishSchemaToDbOptions } from './publish-schema-to-db-options';
|
|
8
|
+
|
|
9
|
+
export class ContentEntityModel {
|
|
10
|
+
public name: string;
|
|
11
|
+
public contentEntity: PgTable;
|
|
12
|
+
public relatedObjects: PgTable[] = [];
|
|
13
|
+
|
|
14
|
+
constructor(options: PublishSchemaToDbOptions, schema: JSONSchema4) {
|
|
15
|
+
if (!schema.title) {
|
|
16
|
+
throw Error('Content type schema title is undefined.');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// TODO: Too restrictive.
|
|
20
|
+
this.name = schema.title.substring(
|
|
21
|
+
0,
|
|
22
|
+
schema.title.indexOf('_published_event'),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// 1. Split properties into primitive and object properties.
|
|
26
|
+
const [primitiveProperties, objectProperties] = separateProperties(
|
|
27
|
+
schema,
|
|
28
|
+
options.ignoredProperties,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// 2. Create the content entity table.
|
|
32
|
+
this.contentEntity = new ContentEntityTable(
|
|
33
|
+
options,
|
|
34
|
+
this.name,
|
|
35
|
+
primitiveProperties,
|
|
36
|
+
schema.description,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// 3. Create related object property tables.
|
|
40
|
+
for (const key in objectProperties) {
|
|
41
|
+
this.relatedObjects.push(
|
|
42
|
+
new ObjectPropertyTable(
|
|
43
|
+
options,
|
|
44
|
+
key,
|
|
45
|
+
objectProperties[key],
|
|
46
|
+
this.contentEntity.pk,
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public validate(): void {
|
|
53
|
+
console.log(`Validating content entity model for ${this.name}`);
|
|
54
|
+
const tables = [this.contentEntity, ...this.relatedObjects];
|
|
55
|
+
for (const table of tables) {
|
|
56
|
+
if (isReservedPgWord(table.name)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Table name ${table.name} is a reserved word. Please specify a name override in the postprocessor.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
for (const column of table.columns) {
|
|
62
|
+
if (isReservedPgWord(column.name)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Column name ${table.name}.${column.name} is a reserved word. Please specify a name override in the postprocessor.`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public buildStatements(): string[] {
|
|
72
|
+
return ([] as string[]).concat(
|
|
73
|
+
`--`,
|
|
74
|
+
`-- #${this.name}`,
|
|
75
|
+
`--`,
|
|
76
|
+
...[this.contentEntity, ...this.relatedObjects].map((t) =>
|
|
77
|
+
t.buildStatements(),
|
|
78
|
+
),
|
|
79
|
+
`--`,
|
|
80
|
+
`--`,
|
|
81
|
+
`--`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public buildSmartTags(): { [name: string]: TableSmartTags } {
|
|
86
|
+
return [this.contentEntity, ...this.relatedObjects].reduce((acc, t) => {
|
|
87
|
+
if (t.buildSmartTags()) {
|
|
88
|
+
acc[t.name] = t.buildSmartTags();
|
|
89
|
+
}
|
|
90
|
+
return acc;
|
|
91
|
+
}, {} as { [name: string]: TableSmartTags });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { JSONPgSmartTags } from 'graphile-utils';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { ContentEntityModel } from './content-entity-model';
|
|
5
|
+
import { getContentMetadataSchemas } from './pg-models/json-schema-parse-utils';
|
|
6
|
+
import { generateMigration } from './pg-models/pg-sql-gen-utils';
|
|
7
|
+
import { generateSmartTagsPlugin } from './pg-models/pgl-utils';
|
|
8
|
+
import {
|
|
9
|
+
collectionPostprocessor,
|
|
10
|
+
ContentEntityModelPostprocessor,
|
|
11
|
+
episodePostprocessor,
|
|
12
|
+
moviePostprocessor,
|
|
13
|
+
seasonPostprocessor,
|
|
14
|
+
tvshowPostprocessor,
|
|
15
|
+
} from './postprocessors';
|
|
16
|
+
import { PublishSchemaToDbOptions } from './publish-schema-to-db-options';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generates a database schema from an asset publish format JSON schema.
|
|
20
|
+
*/
|
|
21
|
+
export async function generate(
|
|
22
|
+
options: PublishSchemaToDbOptions,
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
console.log(`Searching for schemas matching pattern ${options.inputGlob}`);
|
|
25
|
+
|
|
26
|
+
// 1. Read all publish events.
|
|
27
|
+
const contentTypeSchemas = await getContentMetadataSchemas(options.inputGlob);
|
|
28
|
+
const modelPostProcessors: {
|
|
29
|
+
[name: string]: ContentEntityModelPostprocessor;
|
|
30
|
+
} = {
|
|
31
|
+
movie: moviePostprocessor,
|
|
32
|
+
tvshow: tvshowPostprocessor,
|
|
33
|
+
season: seasonPostprocessor,
|
|
34
|
+
episode: episodePostprocessor,
|
|
35
|
+
collection: collectionPostprocessor,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const contentEntityModels: ContentEntityModel[] = [];
|
|
39
|
+
const statements: string[] = [];
|
|
40
|
+
const smartTags: JSONPgSmartTags = {
|
|
41
|
+
version: 1,
|
|
42
|
+
config: {
|
|
43
|
+
class: {},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
for (const contentTypeSchema of contentTypeSchemas) {
|
|
48
|
+
contentEntityModels.push(
|
|
49
|
+
new ContentEntityModel(options, contentTypeSchema),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const model of contentEntityModels) {
|
|
54
|
+
const otherModels = contentEntityModels.filter(
|
|
55
|
+
(m) => m.contentEntity.name !== model.contentEntity.name,
|
|
56
|
+
);
|
|
57
|
+
const postprocess = modelPostProcessors[model.contentEntity.name];
|
|
58
|
+
if (postprocess !== undefined) {
|
|
59
|
+
console.log(`Found model postprocessor for ${model.contentEntity.name}.`);
|
|
60
|
+
postprocess(model, otherModels, options);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
model.validate();
|
|
64
|
+
|
|
65
|
+
smartTags.config.class = {
|
|
66
|
+
...smartTags.config.class,
|
|
67
|
+
...model.buildSmartTags(),
|
|
68
|
+
};
|
|
69
|
+
statements.push(...model.buildStatements());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 3. Generate SQL.
|
|
73
|
+
await generateMigration(
|
|
74
|
+
statements,
|
|
75
|
+
path.join(options.outputRoot, 'migrations', 'current.sql'),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
generateSmartTagsPlugin(
|
|
79
|
+
smartTags,
|
|
80
|
+
path.join(options.outputRoot, 'src', 'plugins', 'smart-tags-plugin.ts'),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { CommandModule } from 'yargs';
|
|
2
|
+
import { generate } from './generate';
|
|
3
|
+
import { PublishSchemaToDbOptions } from './publish-schema-to-db-options';
|
|
4
|
+
|
|
5
|
+
export const publishSchemaToDb: CommandModule<
|
|
6
|
+
unknown,
|
|
7
|
+
PublishSchemaToDbOptions
|
|
8
|
+
> = {
|
|
9
|
+
command: 'publish-schema-to-db',
|
|
10
|
+
describe:
|
|
11
|
+
'Tool to generate the initial database schema for a catalog-like service from publish format definitions.',
|
|
12
|
+
builder: (yargs) =>
|
|
13
|
+
yargs
|
|
14
|
+
.option('inputGlob', {
|
|
15
|
+
// This should be a positional but making it required does not work.
|
|
16
|
+
alias: 'i',
|
|
17
|
+
describe:
|
|
18
|
+
'Glob pattern matching the publish format schemas to be used for code generation.',
|
|
19
|
+
type: 'string',
|
|
20
|
+
demandOption: true,
|
|
21
|
+
})
|
|
22
|
+
.option('idKey', {
|
|
23
|
+
alias: 'k',
|
|
24
|
+
describe: 'Key containing the published ID of a content entity.',
|
|
25
|
+
type: 'string',
|
|
26
|
+
default: 'content_id',
|
|
27
|
+
})
|
|
28
|
+
.option('ignoredProperties', {
|
|
29
|
+
alias: 'n',
|
|
30
|
+
describe: 'List of schema keys to ignore.',
|
|
31
|
+
type: 'array',
|
|
32
|
+
default: ['$schema', 'related_items', 'genres'],
|
|
33
|
+
})
|
|
34
|
+
.option('dbSchema', {
|
|
35
|
+
alias: 's',
|
|
36
|
+
describe: 'Name of the DB schema for SQL generation.',
|
|
37
|
+
type: 'string',
|
|
38
|
+
default: 'app_public',
|
|
39
|
+
})
|
|
40
|
+
.option('outputRoot', {
|
|
41
|
+
alias: 'o',
|
|
42
|
+
describe: 'Path to the catalog service directory.',
|
|
43
|
+
type: 'string',
|
|
44
|
+
demandOption: true,
|
|
45
|
+
}),
|
|
46
|
+
handler: async (argv) => {
|
|
47
|
+
await generate(argv);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
+
const base = require('../../../../../jest.config.base.js');
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
...base,
|
|
6
|
+
displayName: 'publish-schema-to-db',
|
|
7
|
+
testEnvironment: 'node',
|
|
8
|
+
transformIgnorePatterns: ['^.+\\.js$'],
|
|
9
|
+
};
|