Просмотр исходного кода

feat(create): Support schema selection for Postgres

Closes #1662
Michael Bromley 3 лет назад
Родитель
Сommit
217ee7976e

+ 1 - 1
packages/create/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/create",
-  "version": "1.6.5",
+  "version": "1.6.7",
   "license": "MIT",
   "bin": {
     "create": "./index.js"

+ 161 - 135
packages/create/src/create-vendure-app.ts

@@ -18,6 +18,7 @@ import {
     installPackages,
     isSafeToCreateProjectIn,
     isServerPortInUse,
+    scaffoldAlreadyExists,
     shouldUseYarn,
 } from './helpers';
 import { CliLogLevel } from './types';
@@ -75,6 +76,15 @@ async function createApp(
 
     const root = path.resolve(name);
     const appName = path.basename(root);
+    const scaffoldExists = scaffoldAlreadyExists(root, name);
+    if (scaffoldExists) {
+        console.log(
+            chalk.green(
+                `It appears that a new Vendure project scaffold already exists. Re-using the existing files...`,
+            ),
+        );
+        console.log();
+    }
     const {
         dbType,
         usingTs,
@@ -84,7 +94,7 @@ async function createApp(
         migrationSource,
         readmeSource,
         populateProducts,
-    } = isCi ? await gatherCiUserResponses(root) : await gatherUserResponses(root);
+    } = isCi ? await gatherCiUserResponses(root) : await gatherUserResponses(root, scaffoldExists);
 
     const useYarn = useNpm ? false : shouldUseYarn();
     const originalDirectory = process.cwd();
@@ -113,153 +123,169 @@ async function createApp(
     console.log('This may take a few minutes...');
     console.log();
 
-    const tasks = new Listr([
-        {
-            title: 'Installing dependencies',
-            task: (() => {
-                return new Observable(subscriber => {
-                    subscriber.next('Creating package.json');
-                    fs.writeFileSync(
-                        path.join(root, 'package.json'),
-                        JSON.stringify(packageJsonContents, null, 2) + os.EOL,
-                    );
-                    const { dependencies, devDependencies } = getDependencies(
-                        usingTs,
-                        dbType,
-                        isCi ? `@${packageJson.version}` : '',
-                    );
+    const rootPathScript = (fileName: string): string =>
+        path.join(root, `${fileName}.${usingTs ? 'ts' : 'js'}`);
+    const srcPathScript = (fileName: string): string =>
+        path.join(root, 'src', `${fileName}.${usingTs !== false ? 'ts' : 'js'}`);
 
-                    subscriber.next(`Installing ${dependencies.join(', ')}`);
-                    installPackages(root, useYarn, dependencies, false, logLevel, isCi)
-                        .then(() => {
-                            if (devDependencies.length) {
-                                subscriber.next(`Installing ${devDependencies.join(', ')}`);
-                                return installPackages(root, useYarn, devDependencies, true, logLevel, isCi);
-                            }
-                        })
-                        .then(() => subscriber.complete())
-                        .catch(err => subscriber.error(err));
-                });
-            }) as any,
-        },
-        {
-            title: 'Generating app scaffold',
-            task: ctx => {
-                return new Observable(subscriber => {
-                    fs.ensureDirSync(path.join(root, 'src'));
-                    const assetPath = (fileName: string) => path.join(__dirname, '../assets', fileName);
-                    const srcPathScript = (fileName: string): string =>
-                        path.join(root, 'src', `${fileName}.${usingTs ? 'ts' : 'js'}`);
-                    const rootPathScript = (fileName: string): string =>
-                        path.join(root, `${fileName}.${usingTs ? 'ts' : 'js'}`);
-                    ctx.configFile = srcPathScript('vendure-config');
+    const listrTasks: Listr.ListrTask[] = [];
+    if (scaffoldExists) {
+        // ...
+    } else {
+        listrTasks.push(
+            {
+                title: 'Installing dependencies',
+                task: (() => {
+                    return new Observable(subscriber => {
+                        subscriber.next('Creating package.json');
+                        fs.writeFileSync(
+                            path.join(root, 'package.json'),
+                            JSON.stringify(packageJsonContents, null, 2) + os.EOL,
+                        );
+                        const { dependencies, devDependencies } = getDependencies(
+                            usingTs,
+                            dbType,
+                            isCi ? `@${packageJson.version}` : '',
+                        );
 
-                    fs.writeFile(ctx.configFile, configSource)
-                        .then(() => fs.writeFile(srcPathScript('index'), indexSource))
-                        .then(() => fs.writeFile(srcPathScript('index-worker'), indexWorkerSource))
-                        .then(() => fs.writeFile(rootPathScript('migration'), migrationSource))
-                        .then(() => fs.writeFile(path.join(root, 'README.md'), readmeSource))
-                        .then(() =>
-                            fs.copyFile(assetPath('gitignore.template'), path.join(root, '.gitignore')),
-                        )
-                        .then(() => {
-                            subscriber.next(`Created files`);
-                            if (usingTs) {
-                                return fs.copyFile(
-                                    assetPath('tsconfig.template.json'),
-                                    path.join(root, 'tsconfig.json'),
-                                );
-                            }
-                        })
-                        .then(() => createDirectoryStructure(root))
-                        .then(() => {
-                            subscriber.next(`Created directory structure`);
-                            return copyEmailTemplates(root);
-                        })
-                        .then(() => {
-                            subscriber.next(`Copied email templates`);
-                            subscriber.complete();
-                        })
-                        .catch(err => subscriber.error(err));
-                });
+                        subscriber.next(`Installing ${dependencies.join(', ')}`);
+                        installPackages(root, useYarn, dependencies, false, logLevel, isCi)
+                            .then(() => {
+                                if (devDependencies.length) {
+                                    subscriber.next(`Installing ${devDependencies.join(', ')}`);
+                                    return installPackages(
+                                        root,
+                                        useYarn,
+                                        devDependencies,
+                                        true,
+                                        logLevel,
+                                        isCi,
+                                    );
+                                }
+                            })
+                            .then(() => subscriber.complete())
+                            .catch(err => subscriber.error(err));
+                    });
+                }) as any,
             },
-        },
-        {
-            title: 'Initializing server',
-            task: async ctx => {
-                try {
-                    if (usingTs) {
-                        // register ts-node so that the config file can be loaded
-                        require(path.join(root, 'node_modules/ts-node')).register();
-                    }
-                    const { populate } = await import(
-                        path.join(root, 'node_modules/@vendure/core/cli/populate')
-                    );
-                    const { bootstrap, DefaultLogger, LogLevel, JobQueueService } = await import(
-                        path.join(root, 'node_modules/@vendure/core/dist/index')
-                    );
-                    const { config } = await import(ctx.configFile);
-                    const assetsDir = path.join(__dirname, '../assets');
+            {
+                title: 'Generating app scaffold',
+                task: ctx => {
+                    return new Observable(subscriber => {
+                        fs.ensureDirSync(path.join(root, 'src'));
+                        const assetPath = (fileName: string) => path.join(__dirname, '../assets', fileName);
+                        ctx.configFile = srcPathScript('vendure-config');
 
-                    const initialDataPath = path.join(assetsDir, 'initial-data.json');
-                    const port = await detectPort(3000);
-                    const vendureLogLevel =
-                        logLevel === 'silent'
-                            ? LogLevel.Error
-                            : logLevel === 'verbose'
-                            ? LogLevel.Verbose
-                            : LogLevel.Info;
+                        fs.writeFile(ctx.configFile, configSource)
+                            .then(() => fs.writeFile(srcPathScript('index'), indexSource))
+                            .then(() => fs.writeFile(srcPathScript('index-worker'), indexWorkerSource))
+                            .then(() => fs.writeFile(rootPathScript('migration'), migrationSource))
+                            .then(() => fs.writeFile(path.join(root, 'README.md'), readmeSource))
+                            .then(() =>
+                                fs.copyFile(assetPath('gitignore.template'), path.join(root, '.gitignore')),
+                            )
+                            .then(() => {
+                                subscriber.next(`Created files`);
+                                if (usingTs) {
+                                    return fs.copyFile(
+                                        assetPath('tsconfig.template.json'),
+                                        path.join(root, 'tsconfig.json'),
+                                    );
+                                }
+                            })
+                            .then(() => createDirectoryStructure(root))
+                            .then(() => {
+                                subscriber.next(`Created directory structure`);
+                                return copyEmailTemplates(root);
+                            })
+                            .then(() => {
+                                subscriber.next(`Copied email templates`);
+                                subscriber.complete();
+                            })
+                            .catch(err => subscriber.error(err));
+                    });
+                },
+            },
+        );
+    }
+    listrTasks.push({
+        title: 'Initializing server',
+        task: async ctx => {
+            try {
+                if (usingTs) {
+                    // register ts-node so that the config file can be loaded
+                    require(path.join(root, 'node_modules/ts-node')).register();
+                }
+                const { populate } = await import(path.join(root, 'node_modules/@vendure/core/cli/populate'));
+                const { bootstrap, DefaultLogger, LogLevel, JobQueueService } = await import(
+                    path.join(root, 'node_modules/@vendure/core/dist/index')
+                );
+                const configFile = srcPathScript('vendure-config');
+                const { config } = await import(configFile);
+                const assetsDir = path.join(__dirname, '../assets');
+
+                const initialDataPath = path.join(assetsDir, 'initial-data.json');
+                const port = await detectPort(3000);
+                const vendureLogLevel =
+                    logLevel === 'silent'
+                        ? LogLevel.Error
+                        : logLevel === 'verbose'
+                        ? LogLevel.Verbose
+                        : LogLevel.Info;
 
-                    const bootstrapFn = async () => {
-                        await checkDbConnection(config.dbConnectionOptions, root);
-                        const _app = await bootstrap({
-                            ...config,
-                            apiOptions: {
-                                ...(config.apiOptions ?? {}),
-                                port,
-                            },
-                            silent: logLevel === 'silent',
-                            dbConnectionOptions: {
-                                ...config.dbConnectionOptions,
-                                synchronize: true,
-                            },
-                            logger: new DefaultLogger({ level: vendureLogLevel }),
-                            importExportOptions: {
-                                importAssetsDir: path.join(assetsDir, 'images'),
-                            },
-                        });
-                        await _app.get(JobQueueService).start();
-                        return _app;
-                    };
+                const bootstrapFn = async () => {
+                    await checkDbConnection(config.dbConnectionOptions, root);
+                    const _app = await bootstrap({
+                        ...config,
+                        apiOptions: {
+                            ...(config.apiOptions ?? {}),
+                            port,
+                        },
+                        silent: logLevel === 'silent',
+                        dbConnectionOptions: {
+                            ...config.dbConnectionOptions,
+                            synchronize: true,
+                        },
+                        logger: new DefaultLogger({ level: vendureLogLevel }),
+                        importExportOptions: {
+                            importAssetsDir: path.join(assetsDir, 'images'),
+                        },
+                    });
+                    await _app.get(JobQueueService).start();
+                    return _app;
+                };
 
-                    const app = await populate(
-                        bootstrapFn,
-                        initialDataPath,
-                        populateProducts ? path.join(assetsDir, 'products.csv') : undefined,
-                    );
+                const app = await populate(
+                    bootstrapFn,
+                    initialDataPath,
+                    populateProducts ? path.join(assetsDir, 'products.csv') : undefined,
+                );
 
-                    // Pause to ensure the worker jobs have time to complete.
-                    if (isCi) {
-                        console.log('[CI] Pausing before close...');
-                    }
-                    await new Promise(resolve => setTimeout(resolve, isCi ? 30000 : 2000));
-                    await app.close();
-                    if (isCi) {
-                        console.log('[CI] Pausing after close...');
-                        await new Promise(resolve => setTimeout(resolve, 10000));
-                    }
-                } catch (e) {
-                    console.log(e);
-                    throw e;
+                // Pause to ensure the worker jobs have time to complete.
+                if (isCi) {
+                    console.log('[CI] Pausing before close...');
                 }
-            },
+                await new Promise(resolve => setTimeout(resolve, isCi ? 30000 : 2000));
+                await app.close();
+                if (isCi) {
+                    console.log('[CI] Pausing after close...');
+                    await new Promise(resolve => setTimeout(resolve, 10000));
+                }
+            } catch (e) {
+                console.log();
+                console.error(chalk.red(e.message));
+                console.log();
+                console.log(e);
+                throw e;
+            }
         },
-    ]);
+    });
+
+    const tasks = new Listr(listrTasks);
 
     try {
         await tasks.run();
     } catch (e) {
-        console.error(chalk.red(JSON.stringify(e)));
         process.exit(1);
     }
     const startCommand = useYarn ? 'yarn start' : 'npm run start';

+ 93 - 85
packages/create/src/gather-user-responses.ts

@@ -11,7 +11,7 @@ import { DbType, UserResponses } from './types';
 /**
  * Prompts the user to determine how the new Vendure app should be configured.
  */
-export async function gatherUserResponses(root: string): Promise<UserResponses> {
+export async function gatherUserResponses(root: string, alreadyRanScaffold: boolean): Promise<UserResponses> {
     function onSubmit(prompt: PromptObject, answer: any) {
         if (prompt.name === 'dbType') {
             dbType = answer;
@@ -20,91 +20,99 @@ export async function gatherUserResponses(root: string): Promise<UserResponses>
 
     let dbType: DbType;
 
-    const answers = await prompts(
-        [
-            {
-                type: 'select',
-                name: 'dbType',
-                message: 'Which database are you using?',
-                choices: [
-                    { title: 'MySQL', value: 'mysql' },
-                    { title: 'MariaDB', value: 'mariadb' },
-                    { title: 'Postgres', value: 'postgres' },
-                    { title: 'SQLite', value: 'sqlite' },
-                    { title: 'SQL.js', value: 'sqljs' },
-                    // Don't show these until they have been tested.
-                    // { title: 'MS SQL Server', value: 'mssql' },
-                    // { title: 'Oracle', value: 'oracle' },
-                ],
-                initial: 0 as any,
-            },
-            {
-                type: (() => (dbType === 'sqlite' || dbType === 'sqljs' ? null : 'text')) as any,
-                name: 'dbHost',
-                message: `What's the database host address?`,
-                initial: 'localhost',
-            },
-            {
-                type: (() => (dbType === 'sqlite' || dbType === 'sqljs' ? null : 'text')) as any,
-                name: 'dbPort',
-                message: `What port is the database listening on?`,
-                initial: (() => defaultDBPort(dbType)) as any,
-            },
-            {
-                type: (() => (dbType === 'sqlite' || dbType === 'sqljs' ? null : 'text')) as any,
-                name: 'dbName',
-                message: `What's the name of the database?`,
-                initial: 'vendure',
-            },
-            {
-                type: (() => (dbType === 'sqlite' || dbType === 'sqljs' ? null : 'text')) as any,
-                name: 'dbUserName',
-                message: `What's the database user name?`,
-                initial: 'root',
-            },
-            {
-                type: (() => (dbType === 'sqlite' || dbType === 'sqljs' ? null : 'password')) as any,
-                name: 'dbPassword',
-                message: `What's the database password?`,
-            },
-            {
-                type: 'select',
-                name: 'language',
-                message: 'Which programming language will you be using?',
-                choices: [
-                    { title: 'TypeScript', value: 'ts' },
-                    { title: 'JavaScript', value: 'js' },
-                ],
-                initial: 0 as any,
-            },
-            {
-                type: 'toggle',
-                name: 'populateProducts',
-                message: 'Populate with some sample product data?',
-                initial: true,
-                active: 'yes',
-                inactive: 'no',
-            },
-            {
-                type: 'text',
-                name: 'superadminIdentifier',
-                message: 'What identifier do you want to use for the superadmin user?',
-                initial: SUPER_ADMIN_USER_IDENTIFIER,
-            },
-            {
-                type: 'text',
-                name: 'superadminPassword',
-                message: 'What password do you want to use for the superadmin user?',
-                initial: SUPER_ADMIN_USER_PASSWORD,
-            },
-        ],
+    const scaffoldPrompts: Array<prompts.PromptObject<any>> = [
         {
-            onSubmit,
-            onCancel() {
-                /* */
-            },
+            type: 'select',
+            name: 'dbType',
+            message: 'Which database are you using?',
+            choices: [
+                { title: 'MySQL', value: 'mysql' },
+                { title: 'MariaDB', value: 'mariadb' },
+                { title: 'Postgres', value: 'postgres' },
+                { title: 'SQLite', value: 'sqlite' },
+                { title: 'SQL.js', value: 'sqljs' },
+                // Don't show these until they have been tested.
+                // { title: 'MS SQL Server', value: 'mssql' },
+                // { title: 'Oracle', value: 'oracle' },
+            ],
+            initial: 0 as any,
         },
-    );
+        {
+            type: (() => (dbType === 'sqlite' || dbType === 'sqljs' ? null : 'text')) as any,
+            name: 'dbHost',
+            message: `What's the database host address?`,
+            initial: 'localhost',
+        },
+        {
+            type: (() => (dbType === 'sqlite' || dbType === 'sqljs' ? null : 'text')) as any,
+            name: 'dbPort',
+            message: `What port is the database listening on?`,
+            initial: (() => defaultDBPort(dbType)) as any,
+        },
+        {
+            type: (() => (dbType === 'sqlite' || dbType === 'sqljs' ? null : 'text')) as any,
+            name: 'dbName',
+            message: `What's the name of the database?`,
+            initial: 'vendure',
+        },
+        {
+            type: (() => (dbType === 'postgres' ? 'text' : null)) as any,
+            name: 'dbSchema',
+            message: `What's the schema name we should use?`,
+            initial: 'public',
+        },
+        {
+            type: (() => (dbType === 'sqlite' || dbType === 'sqljs' ? null : 'text')) as any,
+            name: 'dbUserName',
+            message: `What's the database user name?`,
+            initial: 'root',
+        },
+        {
+            type: (() => (dbType === 'sqlite' || dbType === 'sqljs' ? null : 'password')) as any,
+            name: 'dbPassword',
+            message: `What's the database password?`,
+        },
+        {
+            type: 'text',
+            name: 'superadminIdentifier',
+            message: 'What identifier do you want to use for the superadmin user?',
+            initial: SUPER_ADMIN_USER_IDENTIFIER,
+        },
+        {
+            type: 'text',
+            name: 'superadminPassword',
+            message: 'What password do you want to use for the superadmin user?',
+            initial: SUPER_ADMIN_USER_PASSWORD,
+        },
+    ];
+
+    const initPrompts: Array<prompts.PromptObject<any>> = [
+        {
+            type: 'select',
+            name: 'language',
+            message: 'Which programming language will you be using?',
+            choices: [
+                { title: 'TypeScript', value: 'ts' },
+                { title: 'JavaScript', value: 'js' },
+            ],
+            initial: 0 as any,
+        },
+        {
+            type: 'toggle',
+            name: 'populateProducts',
+            message: 'Populate with some sample product data?',
+            initial: true,
+            active: 'yes',
+            inactive: 'no',
+        },
+    ];
+
+    const answers = await prompts(alreadyRanScaffold ? initPrompts : [...scaffoldPrompts, ...initPrompts], {
+        onSubmit,
+        onCancel() {
+            /* */
+        },
+    });
 
     if (!answers.language) {
         console.log('Setup aborted. No changes made');
@@ -119,7 +127,7 @@ export async function gatherUserResponses(root: string): Promise<UserResponses>
         configSource,
         migrationSource,
         readmeSource,
-        usingTs: answers.language === 'ts',
+        usingTs: answers.language !== 'js',
         dbType: answers.dbType,
         populateProducts: answers.populateProducts,
         superadminIdentifier: answers.superadminIdentifier,

+ 30 - 0
packages/create/src/helpers.ts

@@ -37,6 +37,14 @@ export function isSafeToCreateProjectIn(root: string, name: string) {
         '.travis.yml',
         '.gitlab-ci.yml',
         '.gitattributes',
+        'migration.ts',
+        'node_modules',
+        'package.json',
+        'package-lock.json',
+        'src',
+        'static',
+        'tsconfig.json',
+        'yarn.lock',
     ];
     console.log();
 
@@ -73,6 +81,12 @@ export function isSafeToCreateProjectIn(root: string, name: string) {
     return true;
 }
 
+export function scaffoldAlreadyExists(root: string, name: string): boolean {
+    const scaffoldFiles = ['migration.ts', 'package.json', 'tsconfig.json', 'README.md'];
+    const files = fs.readdirSync(root);
+    return scaffoldFiles.every(scaffoldFile => files.includes(scaffoldFile));
+}
+
 export function checkNodeVersion(requiredVersion: string) {
     if (!semver.satisfies(process.version, requiredVersion)) {
         console.error(
@@ -324,14 +338,24 @@ async function checkPostgresDbExists(options: any, root: string): Promise<true>
         password: options.password,
         port: options.port,
         database: options.database,
+        schema: options.schema,
     };
     const client = new Client(connectionOptions);
 
     try {
         await client.connect();
+
+        const schema = await client.query(
+            `SELECT schema_name FROM information_schema.schemata WHERE schema_name = '${options.schema}'`,
+        );
+        if (schema.rows.length === 0) {
+            throw new Error('NO_SCHEMA');
+        }
     } catch (e) {
         if (e.code === '3D000') {
             throwDatabaseDoesNotExist(options.database);
+        } else if (e.message === 'NO_SCHEMA') {
+            throwDatabaseSchemaDoesNotExist(options.database, options.schema);
         }
         throwConnectionError(e);
         await client.end();
@@ -354,6 +378,12 @@ function throwDatabaseDoesNotExist(name: string) {
     throw new Error(`Database "${name}" does not exist. Please create the database and then try again.`);
 }
 
+function throwDatabaseSchemaDoesNotExist(dbName: string, schemaName: string) {
+    throw new Error(
+        `Schema "${dbName}.${schemaName}" does not exist. Please create the schema "${schemaName}" and then try again.`,
+    );
+}
+
 export async function isServerPortInUse(): Promise<boolean> {
     const tcpPortUsed = require('tcp-port-used');
     try {

+ 3 - 0
packages/create/templates/vendure-config.hbs

@@ -57,6 +57,9 @@ const path = require('path');
         synchronize: true, // turn this off for production
         logging: false,
         database: {{#if isSQLjs}}new Uint8Array([]){{else if isSQLite}}path.join(__dirname, '../vendure.sqlite'){{else}}'{{{ escapeSingle dbName }}}'{{/if}},
+        {{#if dbSchema}}
+        schema: '{{ dbSchema }}',
+        {{/if}}
         {{#if isSQLjs}}
         location: path.join(__dirname, 'vendure.sqlite'),
         autoSave: true,