create-platformatic 1.17.0 → 1.19.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.
@@ -1,263 +0,0 @@
1
- import { getVersion, getDependencyVersion, safeMkdir } from '../utils.mjs'
2
- import { createPackageJson } from '../create-package-json.mjs'
3
- import { createGitignore } from '../create-gitignore.mjs'
4
- import { getPkgManager } from '@platformatic/utils'
5
- import parseArgs from 'minimist'
6
- import inquirer from 'inquirer'
7
- import which from 'which'
8
- import pino from 'pino'
9
- import pretty from 'pino-pretty'
10
- import { execa } from 'execa'
11
- import ora from 'ora'
12
- import { getConnectionString, createDB } from './create-db.mjs'
13
- import askDir from '../ask-dir.mjs'
14
- import { getUseTypescript, getPort, getInitGitRepository } from '../cli-options.mjs'
15
- import { createReadme } from '../create-readme.mjs'
16
- import { join } from 'node:path'
17
-
18
- const databases = [{
19
- value: 'sqlite',
20
- name: 'SQLite'
21
- }, {
22
- value: 'postgres',
23
- name: 'PostgreSQL'
24
- }, {
25
- value: 'mysql',
26
- name: 'MySQL'
27
- }, {
28
- value: 'mariadb',
29
- name: 'MariaDB'
30
- }]
31
-
32
- export function parseDBArgs (_args) {
33
- return parseArgs(_args, {
34
- default: {
35
- hostname: '127.0.0.1',
36
- database: 'sqlite',
37
- migrations: 'migrations',
38
- install: true
39
- },
40
- alias: {
41
- h: 'hostname',
42
- p: 'port',
43
- pl: 'plugin',
44
- db: 'database',
45
- m: 'migrations',
46
- t: 'types',
47
- ts: 'typescript'
48
- },
49
- boolean: ['plugin', 'types', 'typescript', 'install']
50
- })
51
- }
52
-
53
- const createPlatformaticDB = async (_args, opts) => {
54
- const logger = opts.logger || pino(pretty({
55
- translateTime: 'SYS:HH:MM:ss',
56
- ignore: 'hostname,pid'
57
- }))
58
-
59
- const args = parseDBArgs(_args)
60
- const version = await getVersion()
61
- const pkgManager = getPkgManager()
62
- const projectDir = opts.dir || await askDir(logger, join('.', 'platformatic-db'))
63
-
64
- const isRuntimeContext = opts.isRuntimeContext || false
65
- const toAsk = []
66
-
67
- // Ask for port if not in runtime context
68
- const portQuestion = getPort(args.port)
69
- portQuestion.when = !isRuntimeContext
70
- toAsk.push(portQuestion)
71
-
72
- const { database } = await inquirer.prompt({
73
- type: 'list',
74
- name: 'database',
75
- message: 'What database do you want to use?',
76
- default: args.database,
77
- choices: databases
78
- })
79
-
80
- let connectionString = getConnectionString(database)
81
-
82
- while (true) {
83
- const pickConnectionString = await inquirer.prompt({
84
- type: 'expand',
85
- name: 'edit',
86
- message: `Do you want to use the connection string "${connectionString}"?`,
87
- choices: [
88
- {
89
- key: 'y',
90
- name: 'Confirm',
91
- value: false
92
- },
93
- {
94
- key: 'e',
95
- name: 'Edit',
96
- value: true
97
- }
98
- ]
99
- })
100
-
101
- if (pickConnectionString.edit) {
102
- const answers = await inquirer.prompt({
103
- type: 'editor',
104
- name: 'connectionString',
105
- message: 'Edit the connection string',
106
- default: connectionString
107
- })
108
- connectionString = answers.connectionString.trim()
109
- } else {
110
- break
111
- }
112
- }
113
-
114
- toAsk.push({
115
- type: 'list',
116
- name: 'defaultMigrations',
117
- message: 'Do you want to create default migrations?',
118
- default: true,
119
- choices: [{ name: 'yes', value: true }, { name: 'no', value: false }]
120
- })
121
- toAsk.push({
122
- type: 'list',
123
- name: 'applyMigrations',
124
- message: 'Do you want to apply migrations?',
125
- default: true,
126
- choices: [{ name: 'yes', value: true }, { name: 'no', value: false }],
127
- when: (answers) => {
128
- return answers.defaultMigrations
129
- }
130
- })
131
-
132
- if (args.plugin === false) {
133
- toAsk.push({
134
- type: 'list',
135
- name: 'generatePlugin',
136
- message: 'Do you want to create a plugin?',
137
- default: true,
138
- choices: [{ name: 'yes', value: true }, { name: 'no', value: false }]
139
- })
140
- }
141
-
142
- toAsk.push(getUseTypescript(args.typescript))
143
-
144
- toAsk.push({
145
- type: 'list',
146
- name: 'staticWorkspaceGitHubAction',
147
- message: 'Do you want to create the github action to deploy this application to Platformatic Cloud?',
148
- default: true,
149
- when: !opts.skipGitHubActions,
150
- choices: [{ name: 'yes', value: true }, { name: 'no', value: false }]
151
- },
152
- {
153
- type: 'list',
154
- name: 'dynamicWorkspaceGitHubAction',
155
- message: 'Do you want to enable PR Previews in your application?',
156
- default: true,
157
- when: !opts.skipGitHubActions,
158
- choices: [{ name: 'yes', value: true }, { name: 'no', value: false }]
159
- })
160
- if (!opts.skipGitRepository) {
161
- toAsk.push(getInitGitRepository())
162
- }
163
- // Prompt for questions
164
- const wizardOptions = await inquirer.prompt(toAsk)
165
-
166
- await safeMkdir(projectDir)
167
-
168
- const generatePlugin = args.plugin || wizardOptions.generatePlugin
169
- const useTypescript = args.typescript || wizardOptions.useTypescript
170
- const useTypes = args.types || generatePlugin // we set this always to true if we want to generate a plugin
171
-
172
- const params = {
173
- isRuntimeContext,
174
- hostname: args.hostname,
175
- port: wizardOptions.port,
176
- database,
177
- connectionString,
178
- migrations: wizardOptions.defaultMigrations ? args.migrations : '',
179
- plugin: generatePlugin,
180
- types: useTypes,
181
- typescript: useTypescript,
182
- staticWorkspaceGitHubAction: wizardOptions.staticWorkspaceGitHubAction,
183
- dynamicWorkspaceGitHubAction: wizardOptions.dynamicWorkspaceGitHubAction,
184
- runtimeContext: opts.runtimeContext,
185
- initGitRepository: wizardOptions.initGitRepository
186
- }
187
-
188
- await createDB(params, logger, projectDir, version)
189
-
190
- const fastifyVersion = await getDependencyVersion('fastify')
191
-
192
- const scripts = {
193
- migrate: 'platformatic db migrations apply',
194
- test: useTypescript ? 'tsc && node --test dist/test/*/*.test.js' : 'node --test test/*/*.test.js'
195
- }
196
-
197
- const dependencies = {
198
- '@platformatic/db': `^${version}`
199
- }
200
-
201
- // Create the package.json, .gitignore, readme
202
- await createPackageJson(version, fastifyVersion, logger, projectDir, useTypescript, scripts, dependencies)
203
- await createGitignore(logger, projectDir)
204
- await createReadme(logger, projectDir, 'db')
205
-
206
- let hasPlatformaticInstalled = false
207
- if (args.install && !opts.skipPackageJson) {
208
- const spinner = ora('Installing dependencies...').start()
209
- await execa(pkgManager, ['install'], { cwd: projectDir })
210
- spinner.succeed()
211
- hasPlatformaticInstalled = true
212
- }
213
-
214
- if (!hasPlatformaticInstalled) {
215
- try {
216
- const npmLs = JSON.parse(await execa('npm', ['ls', '--json']).toString())
217
- hasPlatformaticInstalled = !!npmLs.dependencies.platformatic
218
- } catch {
219
- // Ignore all errors, this can fail
220
- }
221
- }
222
-
223
- if (!hasPlatformaticInstalled) {
224
- const exe = await which('platformatic', { nothrow: true })
225
- hasPlatformaticInstalled = !!exe
226
- }
227
-
228
- if (hasPlatformaticInstalled) {
229
- // We applied package manager install, so we can:
230
- // - run the migrations
231
- // - generate types
232
- // if we don't generate migrations, we don't ask to apply them (the folder might not exist)
233
- let migrationApplied = false
234
- if (wizardOptions.defaultMigrations && wizardOptions.applyMigrations) {
235
- const spinner = ora('Applying migrations...').start()
236
- // We need to apply migrations using the platformatic installed in the project
237
- try {
238
- await execa(pkgManager, ['exec', 'platformatic', 'db', 'migrations', 'apply'], { cwd: projectDir })
239
- spinner.succeed()
240
- migrationApplied = true
241
- } catch (err) {
242
- logger.trace({ err })
243
- spinner.fail('Failed applying migrations! Try again by running "platformatic db migrations apply"')
244
- }
245
- }
246
- if (generatePlugin && migrationApplied) {
247
- const spinner = ora('Generating types...').start()
248
- try {
249
- await execa(pkgManager, ['exec', 'platformatic', 'db', 'types'], { cwd: projectDir })
250
- spinner.succeed()
251
- } catch (err) {
252
- logger.trace({ err })
253
- spinner.fail('Failed to generate Types. Try again by running "platformatic service types"')
254
- }
255
- }
256
- }
257
- // returns metadata that can be used to make some further actions
258
- return {
259
- typescript: useTypescript
260
- }
261
- }
262
-
263
- export default createPlatformaticDB
@@ -1,449 +0,0 @@
1
- import { writeFile, appendFile } from 'fs/promises'
2
- import { join } from 'path'
3
- import { addPrefixToEnv, safeMkdir } from '../utils.mjs'
4
- import { getTsConfig } from '../get-tsconfig.mjs'
5
- import { generatePlugins } from '../create-plugins.mjs'
6
- import { createDynamicWorkspaceGHAction, createStaticWorkspaceGHAction } from '../ghaction.mjs'
7
- import { createGitRepository } from '../create-git-repository.mjs'
8
-
9
- const connectionStrings = {
10
- postgres: 'postgres://postgres:postgres@127.0.0.1:5432/postgres',
11
- sqlite: 'sqlite://./db.sqlite',
12
- mysql: 'mysql://root@127.0.0.1:3306/platformatic',
13
- mariadb: 'mysql://root@127.0.0.1:3306/platformatic'
14
- }
15
-
16
- const moviesMigrationDo = (database) => {
17
- const key = {
18
- postgres: 'SERIAL',
19
- sqlite: 'INTEGER',
20
- mysql: 'INTEGER UNSIGNED AUTO_INCREMENT',
21
- mariadb: 'INTEGER UNSIGNED AUTO_INCREMENT'
22
- }
23
-
24
- return `
25
- -- Add SQL in this file to create the database tables for your API
26
- CREATE TABLE IF NOT EXISTS movies (
27
- id ${key[database]} PRIMARY KEY,
28
- title TEXT NOT NULL
29
- );
30
- `
31
- }
32
-
33
- const moviesMigrationUndo = `
34
- -- Add SQL in this file to drop the database tables
35
- DROP TABLE movies;
36
- `
37
-
38
- const TS_OUT_DIR = 'dist'
39
-
40
- const jsHelperSqlite = {
41
- requires: `
42
- const os = require('node:os')
43
- const path = require('node:path')
44
- const fs = require('node:fs/promises')
45
-
46
- let counter = 0
47
- `,
48
- pre: `
49
- const dbPath = join(os.tmpdir(), 'db-' + process.pid + '-' + counter++ + '.sqlite')
50
- const connectionString = 'sqlite://' + dbPath
51
- `,
52
- config: `
53
- config.migrations.autoApply = true
54
- config.types.autogenerate = false
55
- config.db.connectionString = connectionString
56
- `,
57
- post: `
58
- t.after(async () => {
59
- await fs.unlink(dbPath)
60
- })
61
- `
62
- }
63
-
64
- function jsHelperPostgres (connectionString) {
65
- return {
66
- // TODO(mcollina): replace sql-mapper
67
- requires: `
68
- const { createConnectionPool } = require('@platformatic/sql-mapper')
69
- const connectionString = '${connectionString}'
70
- let counter = 0
71
- `,
72
- pre: `
73
- const { db, sql } = await createConnectionPool({
74
- log: {
75
- debug: () => {},
76
- info: () => {},
77
- trace: () => {}
78
- },
79
- connectionString,
80
- poolSize: 1
81
- })
82
-
83
- const newDB = \`t-\${process.pid}-\${counter++}\`
84
- t.diagnostic('Creating database ' + newDB)
85
-
86
- await db.query(sql\`
87
- CREATE DATABASE \${sql.ident(newDB)}
88
- \`)
89
- `,
90
- config: `
91
- config.migrations.autoApply = true
92
- config.types.autogenerate = false
93
- config.db.connectionString = connectionString.replace(/\\/[a-zA-Z0-9\\-_]+$/, '/' + newDB)
94
- config.db.schemalock = false
95
- `,
96
- post: `
97
- t.after(async () => {
98
- t.diagnostic('Disposing test database ' + newDB)
99
- await db.query(sql\`
100
- DROP DATABASE \${sql.ident(newDB)}
101
- \`)
102
- await db.dispose()
103
- })
104
- `
105
- }
106
- }
107
-
108
- function jsHelperMySQL (connectionString) {
109
- return {
110
- // TODO(mcollina): replace sql-mapper
111
- requires: `
112
- const { createConnectionPool } = require('@platformatic/sql-mapper')
113
- const connectionString = '${connectionString}'
114
- let counter = 0
115
- `,
116
- pre: `
117
- const { db, sql } = await createConnectionPool({
118
- log: {
119
- debug: () => {},
120
- info: () => {},
121
- trace: () => {}
122
- },
123
- connectionString,
124
- poolSize: 1
125
- })
126
-
127
- const newDB = \`t-\${process.pid}-\${counter++}\`
128
- t.diagnostic('Creating database ' + newDB)
129
-
130
- await db.query(sql\`
131
- CREATE DATABASE \${sql.ident(newDB)}
132
- \`)
133
- `,
134
- config: `
135
- config.migrations.autoApply = true
136
- config.types.autogenerate = false
137
- config.db.connectionString = connectionString.replace(/\\/[a-zA-Z0-9\\-_]+$/, '/' + newDB)
138
- config.db.schemalock = false
139
- `,
140
- post: `
141
- t.after(async () => {
142
- t.diagnostic('Disposing test database ' + newDB)
143
- await db.query(sql\`
144
- DROP DATABASE \${sql.ident(newDB)}
145
- \`)
146
- await db.dispose()
147
- })
148
- `
149
- }
150
- }
151
-
152
- const moviesTestJS = `\
153
- 'use strict'
154
-
155
- const test = require('node:test')
156
- const assert = require('node:assert')
157
- const { getServer } = require('../helper')
158
-
159
- test('movies', async (t) => {
160
- const server = await getServer(t)
161
-
162
- {
163
- const res = await server.inject({
164
- method: 'GET',
165
- url: '/movies'
166
- })
167
-
168
- assert.strictEqual(res.statusCode, 200)
169
- assert.deepStrictEqual(res.json(), [])
170
- }
171
-
172
- let id
173
- {
174
- const res = await server.inject({
175
- method: 'POST',
176
- url: '/movies',
177
- body: {
178
- title: 'The Matrix'
179
- }
180
- })
181
-
182
- assert.strictEqual(res.statusCode, 200)
183
- const body = res.json()
184
- assert.strictEqual(body.title, 'The Matrix')
185
- assert.strictEqual(body.id !== undefined, true)
186
- id = body.id
187
- }
188
-
189
- {
190
- const res = await server.inject({
191
- method: 'GET',
192
- url: '/movies'
193
- })
194
-
195
- assert.strictEqual(res.statusCode, 200)
196
- assert.deepStrictEqual(res.json(), [{
197
- id,
198
- title: 'The Matrix'
199
- }])
200
- }
201
- })
202
- `
203
-
204
- const moviesTestTS = `\
205
- import test from 'node:test'
206
- import assert from 'node:assert'
207
- import { getServer } from '../helper'
208
-
209
- test('movies', async (t) => {
210
- const server = await getServer(t)
211
-
212
- {
213
- const res = await server.inject({
214
- method: 'GET',
215
- url: '/movies'
216
- })
217
-
218
- assert.strictEqual(res.statusCode, 200)
219
- assert.deepStrictEqual(res.json(), [])
220
- }
221
-
222
- let id : Number
223
- {
224
- const res = await server.inject({
225
- method: 'POST',
226
- url: '/movies',
227
- body: {
228
- title: 'The Matrix'
229
- }
230
- })
231
-
232
- assert.strictEqual(res.statusCode, 200)
233
- const body = res.json()
234
- assert.strictEqual(body.title, 'The Matrix')
235
- assert.strictEqual(body.id !== undefined, true)
236
- id = body.id as Number
237
- }
238
-
239
- {
240
- const res = await server.inject({
241
- method: 'GET',
242
- url: '/movies'
243
- })
244
-
245
- assert.strictEqual(res.statusCode, 200)
246
- assert.deepStrictEqual(res.json(), [{
247
- id,
248
- title: 'The Matrix'
249
- }])
250
- }
251
- })
252
- `
253
-
254
- function generateConfig (isRuntimeContext, migrations, plugin, types, typescript, version, envPrefix) {
255
- const connectionStringValue = envPrefix ? `PLT_${envPrefix}DATABASE_URL` : 'DATABASE_URL'
256
- const config = {
257
- $schema: `https://platformatic.dev/schemas/v${version}/db`,
258
- db: {
259
- connectionString: `{${connectionStringValue}}`,
260
- graphql: true,
261
- openapi: true,
262
- schemalock: true
263
- },
264
- watch: {
265
- ignore: ['*.sqlite', '*.sqlite-journal']
266
- }
267
- }
268
-
269
- if (!isRuntimeContext) {
270
- config.server = {
271
- hostname: '{PLT_SERVER_HOSTNAME}',
272
- port: '{PORT}',
273
- logger: {
274
- level: '{PLT_SERVER_LOGGER_LEVEL}'
275
- }
276
- }
277
- }
278
-
279
- if (migrations) {
280
- config.migrations = {
281
- dir: migrations
282
- }
283
- }
284
-
285
- if (plugin === true) {
286
- config.plugins = {
287
- paths: [{
288
- path: './plugins',
289
- encapsulate: false
290
- }, {
291
- path: './routes'
292
- }]
293
- }
294
- }
295
-
296
- if (types === true) {
297
- config.types = {
298
- autogenerate: true
299
- }
300
- }
301
-
302
- if (typescript === true && config.plugins) {
303
- config.plugins.typescript = `{PLT_${envPrefix}TYPESCRIPT}`
304
- }
305
-
306
- return config
307
- }
308
-
309
- function generateEnv (isRuntimeContext, hostname, port, connectionString, typescript, envPrefix) {
310
- let env = ''
311
- if (envPrefix) {
312
- env += `PLT_${envPrefix}`
313
- }
314
- env += `DATABASE_URL=${connectionString}\n`
315
-
316
- if (!isRuntimeContext) {
317
- env += `\
318
- PLT_SERVER_HOSTNAME=${hostname}
319
- PORT=${port}
320
- PLT_SERVER_LOGGER_LEVEL=info
321
-
322
- `
323
- }
324
-
325
- if (typescript === true) {
326
- env += `\
327
- # Set to false to disable automatic typescript compilation.
328
- # Changing this setting is needed for production
329
- PLT_${envPrefix}TYPESCRIPT=true
330
- `
331
- }
332
-
333
- return env
334
- }
335
-
336
- export function getConnectionString (database) {
337
- return connectionStrings[database]
338
- }
339
-
340
- export async function createDB (params, logger, currentDir, version) {
341
- let {
342
- isRuntimeContext,
343
- hostname,
344
- port,
345
- database = 'sqlite',
346
- migrations = 'migrations',
347
- plugin = true,
348
- types = true,
349
- typescript = false,
350
- connectionString,
351
- staticWorkspaceGitHubAction,
352
- dynamicWorkspaceGitHubAction,
353
- runtimeContext,
354
- initGitRepository
355
- } = params
356
-
357
- const dbEnv = {
358
- DATABASE_URL: connectionString,
359
- PLT_SERVER_LOGGER_LEVEL: 'info',
360
- PORT: port,
361
- PLT_SERVER_HOSTNAME: hostname
362
- }
363
-
364
- if (typescript) {
365
- dbEnv.PLT_TYPESCRIPT = true
366
- }
367
- connectionString = connectionString || getConnectionString(database)
368
- const createMigrations = !!migrations // If we don't define a migrations folder, we don't create it
369
- const envPrefix = runtimeContext !== undefined ? `${runtimeContext.envPrefix}_` : ''
370
-
371
- const config = generateConfig(isRuntimeContext, migrations, plugin, types, typescript, version, envPrefix)
372
- await writeFile(join(currentDir, 'platformatic.db.json'), JSON.stringify(config, null, 2))
373
- logger.info('Configuration file platformatic.db.json successfully created.')
374
-
375
- const env = generateEnv(isRuntimeContext, hostname, port, connectionString, typescript, envPrefix)
376
- const envSample = generateEnv(isRuntimeContext, hostname, port, getConnectionString(database), typescript, envPrefix)
377
- await appendFile(join(currentDir, '.env'), env)
378
- await writeFile(join(currentDir, '.env.sample'), envSample)
379
-
380
- const migrationsFolderName = migrations
381
- if (createMigrations) {
382
- await safeMkdir(join(currentDir, migrationsFolderName))
383
- logger.info(`Migrations folder ${migrationsFolderName} successfully created.`)
384
- }
385
-
386
- const migrationFileNameDo = '001.do.sql'
387
- const migrationFileNameUndo = '001.undo.sql'
388
- const migrationFilePathDo = join(currentDir, migrationsFolderName, migrationFileNameDo)
389
- const migrationFilePathUndo = join(currentDir, migrationsFolderName, migrationFileNameUndo)
390
- if (createMigrations) {
391
- await writeFile(migrationFilePathDo, moviesMigrationDo(database))
392
- logger.info(`Migration file ${migrationFileNameDo} successfully created.`)
393
- await writeFile(migrationFilePathUndo, moviesMigrationUndo)
394
- logger.info(`Migration file ${migrationFileNameUndo} successfully created.`)
395
- }
396
-
397
- if (typescript === true) {
398
- const tsConfigFileName = join(currentDir, 'tsconfig.json')
399
- const tsConfig = getTsConfig(TS_OUT_DIR)
400
- await writeFile(tsConfigFileName, JSON.stringify(tsConfig, null, 2))
401
- logger.info(`Typescript configuration file ${tsConfigFileName} successfully created.`)
402
- }
403
-
404
- if (plugin) {
405
- let jsHelper = { pre: '', config: '', post: '' }
406
- switch (database) {
407
- case 'sqlite':
408
- jsHelper = jsHelperSqlite
409
- break
410
- case 'mysql':
411
- jsHelper = jsHelperMySQL(connectionString)
412
- break
413
- case 'postgres':
414
- jsHelper = jsHelperPostgres(connectionString)
415
- break
416
- case 'mariadb':
417
- jsHelper = jsHelperMySQL(connectionString)
418
- break
419
- }
420
- await generatePlugins(logger, currentDir, typescript, 'db', jsHelper)
421
-
422
- if (createMigrations) {
423
- if (typescript) {
424
- await writeFile(join(currentDir, 'test', 'routes', 'movies.test.ts'), moviesTestTS)
425
- } else {
426
- await writeFile(join(currentDir, 'test', 'routes', 'movies.test.js'), moviesTestJS)
427
- }
428
- }
429
- }
430
-
431
- if (staticWorkspaceGitHubAction) {
432
- await createStaticWorkspaceGHAction(logger, dbEnv, './platformatic.db.json', currentDir, typescript)
433
- }
434
- if (dynamicWorkspaceGitHubAction) {
435
- await createDynamicWorkspaceGHAction(logger, dbEnv, './platformatic.db.json', currentDir, typescript)
436
- }
437
-
438
- if (initGitRepository) {
439
- await createGitRepository(logger, currentDir)
440
- }
441
-
442
- if (isRuntimeContext) {
443
- return addPrefixToEnv(isRuntimeContext)
444
- }
445
-
446
- return dbEnv
447
- }
448
-
449
- export default createDB