create-platformatic 0.11.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.
@@ -0,0 +1,200 @@
1
+ import { writeFile, mkdir } from 'fs/promises'
2
+ import { join, relative, resolve } from 'path'
3
+ import { findDBConfigFile, isFileAccessible } from '../utils.mjs'
4
+
5
+ const connectionStrings = {
6
+ postgres: 'postgres://postgres:postgres@localhost:5432/postgres',
7
+ sqlite: 'sqlite://./db.sqlite',
8
+ mysql: 'mysql://root@localhost:3306/graph',
9
+ mysql8: 'mysql://root@localhost:3308/graph',
10
+ mariadb: 'mysql://root@localhost:3307/graph'
11
+ }
12
+
13
+ const moviesMigrationDo = `
14
+ -- Add SQL in this file to create the database tables for your API
15
+ CREATE TABLE IF NOT EXISTS movies (
16
+ id INTEGER PRIMARY KEY,
17
+ title TEXT NOT NULL
18
+ );
19
+ `
20
+
21
+ const moviesMigrationUndo = `
22
+ -- Add SQL in this file to drop the database tables
23
+ DROP TABLE movies;
24
+ `
25
+
26
+ function getTsConfig (outDir) {
27
+ return {
28
+ compilerOptions: {
29
+ module: 'commonjs',
30
+ esModuleInterop: true,
31
+ target: 'es6',
32
+ sourceMap: true,
33
+ pretty: true,
34
+ noEmitOnError: true,
35
+ outDir
36
+ },
37
+ watchOptions: {
38
+ watchFile: 'fixedPollingInterval',
39
+ watchDirectory: 'fixedPollingInterval',
40
+ fallbackPolling: 'dynamicPriority',
41
+ synchronousWatchDirectory: true,
42
+ excludeDirectories: ['**/node_modules', outDir]
43
+ }
44
+ }
45
+ }
46
+
47
+ const getPluginName = (isTypescript) => isTypescript === true ? 'plugin.ts' : 'plugin.js'
48
+ const TS_OUT_DIR = 'dist'
49
+
50
+ function generateConfig (migrations, plugin, types, typescript) {
51
+ const config = {
52
+ $schema: './platformatic.db.schema.json',
53
+ server: {
54
+ hostname: '{PLT_SERVER_HOSTNAME}',
55
+ port: '{PORT}',
56
+ logger: {
57
+ level: '{PLT_SERVER_LOGGER_LEVEL}'
58
+ }
59
+ },
60
+ core: {
61
+ connectionString: '{DATABASE_URL}',
62
+ graphql: true,
63
+ openapi: true
64
+ },
65
+ migrations: { dir: migrations }
66
+ }
67
+
68
+ if (plugin === true) {
69
+ config.plugin = {
70
+ path: getPluginName(typescript)
71
+ }
72
+ }
73
+
74
+ if (types === true) {
75
+ config.types = {
76
+ autogenerate: true
77
+ }
78
+ }
79
+
80
+ if (typescript === true) {
81
+ config.plugin.typescript = {
82
+ outDir: TS_OUT_DIR
83
+ }
84
+ }
85
+
86
+ return config
87
+ }
88
+
89
+ function generateEnv (hostname, port, database) {
90
+ const connectionString = connectionStrings[database]
91
+ const env = `\
92
+ PLT_SERVER_HOSTNAME=${hostname}
93
+ PORT=${port}
94
+ PLT_SERVER_LOGGER_LEVEL=info
95
+ DATABASE_URL=${connectionString}
96
+ `
97
+ return env
98
+ }
99
+
100
+ const JS_PLUGIN_WITH_TYPES_SUPPORT = `\
101
+ /// <reference path="./global.d.ts" />
102
+ 'use strict'
103
+
104
+ /** @param {import('fastify').FastifyInstance} app */
105
+ module.exports = async function (app) {}
106
+ `
107
+
108
+ const TS_PLUGIN_WITH_TYPES_SUPPORT = `\
109
+ /// <reference path="./global.d.ts" />
110
+ import { FastifyInstance } from 'fastify'
111
+
112
+ export default async function (app: FastifyInstance) {}
113
+ `
114
+
115
+ async function generatePluginWithTypesSupport (logger, currentDir, isTypescript) {
116
+ const pluginPath = resolve(currentDir, getPluginName(isTypescript))
117
+
118
+ const isPluginExists = await isFileAccessible(pluginPath)
119
+ if (isPluginExists) {
120
+ logger.info(`Plugin file ${pluginPath} found, skipping creation of plugin file.`)
121
+ return
122
+ }
123
+
124
+ const pluginTemplate = isTypescript
125
+ ? TS_PLUGIN_WITH_TYPES_SUPPORT
126
+ : JS_PLUGIN_WITH_TYPES_SUPPORT
127
+
128
+ await writeFile(pluginPath, pluginTemplate)
129
+ logger.info(`Plugin file created at ${relative(currentDir, pluginPath)}`)
130
+ }
131
+
132
+ async function createDB ({ hostname, database = 'sqlite', port, migrations = 'migrations', plugin = true, types = true, typescript = false }, logger, currentDir) {
133
+ const createMigrations = !!migrations // If we don't define a migrations folder, we don't create it
134
+ const accessibleConfigFilename = await findDBConfigFile(currentDir)
135
+ if (accessibleConfigFilename === undefined) {
136
+ const config = generateConfig(migrations, plugin, types, typescript)
137
+ await writeFile(join(currentDir, 'platformatic.db.json'), JSON.stringify(config, null, 2))
138
+ logger.info('Configuration file platformatic.db.json successfully created.')
139
+
140
+ const env = generateEnv(hostname, port, database)
141
+ await writeFile(join(currentDir, '.env'), env)
142
+ await writeFile(join(currentDir, '.env.sample'), env)
143
+ logger.info('Environment file .env successfully created.')
144
+ } else {
145
+ logger.info(`Configuration file ${accessibleConfigFilename} found, skipping creation of configuration file.`)
146
+ }
147
+
148
+ const migrationsFolderName = migrations
149
+ if (createMigrations) {
150
+ const isMigrationFolderExists = await isFileAccessible(migrationsFolderName, currentDir)
151
+ if (!isMigrationFolderExists) {
152
+ await mkdir(join(currentDir, migrationsFolderName))
153
+ logger.info(`Migrations folder ${migrationsFolderName} successfully created.`)
154
+ } else {
155
+ logger.info(`Migrations folder ${migrationsFolderName} found, skipping creation of migrations folder.`)
156
+ }
157
+ }
158
+
159
+ const migrationFileNameDo = '001.do.sql'
160
+ const migrationFileNameUndo = '001.undo.sql'
161
+ const migrationFilePathDo = join(currentDir, migrationsFolderName, migrationFileNameDo)
162
+ const migrationFilePathUndo = join(currentDir, migrationsFolderName, migrationFileNameUndo)
163
+ const isMigrationFileDoExists = await isFileAccessible(migrationFilePathDo)
164
+ const isMigrationFileUndoExists = await isFileAccessible(migrationFilePathUndo)
165
+ if (!isMigrationFileDoExists && createMigrations) {
166
+ await writeFile(migrationFilePathDo, moviesMigrationDo)
167
+ logger.info(`Migration file ${migrationFileNameDo} successfully created.`)
168
+ if (!isMigrationFileUndoExists) {
169
+ await writeFile(migrationFilePathUndo, moviesMigrationUndo)
170
+ logger.info(`Migration file ${migrationFileNameUndo} successfully created.`)
171
+ }
172
+ } else {
173
+ logger.info(`Migration file ${migrationFileNameDo} found, skipping creation of migration file.`)
174
+ }
175
+
176
+ if (typescript === true) {
177
+ const tsConfigFileName = join(currentDir, 'tsconfig.json')
178
+ const isTsConfigExists = await isFileAccessible(tsConfigFileName)
179
+ if (!isTsConfigExists) {
180
+ const tsConfig = getTsConfig(TS_OUT_DIR)
181
+ await writeFile(tsConfigFileName, JSON.stringify(tsConfig, null, 2))
182
+ logger.info(`Typescript configuration file ${tsConfigFileName} successfully created.`)
183
+ } else {
184
+ logger.info(`Typescript configuration file ${tsConfigFileName} found, skipping creation of typescript configuration file.`)
185
+ }
186
+ }
187
+
188
+ if (plugin) {
189
+ await generatePluginWithTypesSupport(logger, currentDir, typescript)
190
+ }
191
+
192
+ return {
193
+ DATABASE_URL: connectionStrings[database],
194
+ PLT_SERVER_LOGGER_LEVEL: 'info',
195
+ PORT: port,
196
+ PLT_SERVER_HOSTNAME: hostname
197
+ }
198
+ }
199
+
200
+ export default createDB
@@ -0,0 +1,11 @@
1
+
2
+ export const getPkgManager = () => {
3
+ const userAgent = process.env.npm_config_user_agent
4
+ if (!userAgent) {
5
+ return 'npm'
6
+ }
7
+ const pmSpec = userAgent.split(' ')[0]
8
+ const separatorPos = pmSpec.lastIndexOf('/')
9
+ const name = pmSpec.substring(0, separatorPos)
10
+ return name || 'npm'
11
+ }
@@ -0,0 +1,68 @@
1
+ import mkdirp from 'mkdirp'
2
+ import { join } from 'path'
3
+ import inquirer from 'inquirer'
4
+ import { isFileAccessible } from './utils.mjs'
5
+ import { writeFile } from 'fs/promises'
6
+
7
+ export const ghTemplate = (env, type) => {
8
+ const envAsStr = Object.keys(env).reduce((acc, key) => {
9
+ acc += ` ${key}: ${env[key]} \n`
10
+ return acc
11
+ }, '')
12
+
13
+ return `name: Deploy Platformatic application to the cloud
14
+ on:
15
+ pull_request:
16
+ paths-ignore:
17
+ - 'docs/**'
18
+ - '**.md'
19
+
20
+ jobs:
21
+ build_and_deploy:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - name: Checkout application project repository
25
+ uses: actions/checkout@v3
26
+ - name: npm install --omit=dev
27
+ run: npm install --omit=dev
28
+ - name: Deploy project
29
+ uses: platformatic/onestep@latest
30
+ with:
31
+ github_token: \${{ secrets.GITHUB_TOKEN }}
32
+ platformatic_api_key: \${{ secrets.PLATFORMATIC_API_KEY }}
33
+ platformatic_config_path: ./platformatic.${type}.json
34
+ env:
35
+ ${envAsStr}
36
+ `
37
+ }
38
+
39
+ export const createGHAction = async (logger, env, type, projectDir) => {
40
+ const ghActionFileName = 'platformatic-deploy.yml'
41
+ const ghActionFilePath = join(projectDir, '.github', 'workflows', ghActionFileName)
42
+ const isGithubActionExists = await isFileAccessible(ghActionFilePath)
43
+ if (!isGithubActionExists) {
44
+ await mkdirp(join(projectDir, '.github', 'workflows'))
45
+ await writeFile(ghActionFilePath, ghTemplate(env, type))
46
+ logger.info('Github action successfully created, please add PLATFORMATIC_API_KEY as repository secret.')
47
+ const isGitDir = await isFileAccessible('.git', projectDir)
48
+ if (!isGitDir) {
49
+ logger.warn('No git repository found. The Github action won\'t be triggered.')
50
+ }
51
+ } else {
52
+ logger.info(`Github action file ${ghActionFilePath} found, skipping creation of github action file.`)
53
+ }
54
+ }
55
+
56
+ /* c8 ignore next 12 */
57
+ export const askCreateGHAction = async (logger, env, type, projectDir = process.cwd()) => {
58
+ const { githubAction } = await inquirer.prompt([{
59
+ type: 'list',
60
+ name: 'githubAction',
61
+ message: 'Do you want to create the github action to deploy this application to Platformatic Cloud?',
62
+ default: true,
63
+ choices: [{ name: 'yes', value: true }, { name: 'no', value: false }]
64
+ }])
65
+ if (githubAction) {
66
+ await createGHAction(logger, env, type, projectDir)
67
+ }
68
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,51 @@
1
+ import { say } from './say.mjs'
2
+ import helpMe from 'help-me'
3
+ import { join } from 'desm'
4
+ import inquirer from 'inquirer'
5
+ import createPlatformaticDB from './db/create-db-cli.mjs'
6
+ import createPlatformaticService from './service/create-service-cli.mjs'
7
+ import commist from 'commist'
8
+ import { getUsername, getVersion } from './utils.mjs'
9
+
10
+ const createPlatformatic = async (argv) => {
11
+ const help = helpMe({
12
+ dir: join(import.meta.url, '..', 'help'),
13
+ ext: '.txt'
14
+ })
15
+
16
+ const program = commist({ maxDistance: 4 })
17
+
18
+ program.register('help', help.toStdout)
19
+ program.register('db help', help.toStdout.bind(null, ['db']))
20
+ program.register('service help', help.toStdout.bind(null, ['service']))
21
+ program.register('db', createPlatformaticDB)
22
+ program.register('service', createPlatformaticService)
23
+
24
+ const result = program.parse(argv)
25
+
26
+ if (result) {
27
+ const username = await getUsername()
28
+ const version = await getVersion()
29
+ const greeting = username ? `Hello, ${username}` : 'Hello,'
30
+ await say(`${greeting} welcome to ${version ? `Platformatic ${version}!` : 'Platformatic!'}`)
31
+ await say('Let\'s start by creating a new project.')
32
+
33
+ const options = await inquirer.prompt({
34
+ type: 'list',
35
+ name: 'type',
36
+ message: 'Which kind of project do you want to create?',
37
+ default: 'db',
38
+ choices: [{ name: 'DB', value: 'db' }, { name: 'Service', value: 'service' }]
39
+ })
40
+
41
+ if (options.type === 'db') {
42
+ await createPlatformaticDB(argv)
43
+ } else if (options.type === 'service') {
44
+ await createPlatformaticService(argv)
45
+ }
46
+
47
+ await say('\nAll done! Please open the project directory and check the README.')
48
+ }
49
+ }
50
+
51
+ export default createPlatformatic
package/src/say.mjs ADDED
@@ -0,0 +1,20 @@
1
+ import logUpdate from 'log-update'
2
+ import { pltGreen } from './colors.mjs'
3
+ import { sleep, randomBetween } from './utils.mjs'
4
+
5
+ export const say = async (messages) => {
6
+ const _messages = Array.isArray(messages) ? messages : [messages]
7
+
8
+ for (const message of _messages) {
9
+ const _message = Array.isArray(message) ? message : message.split(' ')
10
+ const msg = []
11
+ for (const word of [''].concat(_message)) {
12
+ msg.push(word)
13
+ logUpdate(pltGreen(msg.join(' ')))
14
+ await sleep(randomBetween(75, 100))
15
+ process.stdout.write('\u0007') // Do we want to enable terminal bell?
16
+ }
17
+ await sleep(randomBetween(75, 200))
18
+ }
19
+ logUpdate.done()
20
+ }
@@ -0,0 +1,31 @@
1
+ # Platformatic Service API
2
+
3
+ This is a generated [Platformatic DB](https://oss.platformatic.dev/docs/reference/service/introduction) application.
4
+
5
+ ## Requirements
6
+
7
+ Platformatic supports macOS, Linux and Windows ([WSL](https://docs.microsoft.com/windows/wsl/) recommended).
8
+ You'll need to have [Node.js](https://nodejs.org/) >= v16.17.0 or >= v18.8.0
9
+
10
+ ## Setup
11
+
12
+ Install dependencies:
13
+
14
+ ```bash
15
+ npm install
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ Run the API with:
21
+
22
+ ```bash
23
+ npm start
24
+ ```
25
+
26
+ ### Explore
27
+ - ⚡ The Platformatic DB server is running at http://localhost:3042/
28
+ - 📔 View the REST API's Swagger documentation at http://localhost:3042/documentation/
29
+ - 🔍 Try out the GraphiQL web UI at http://localhost:3042/graphiql
30
+
31
+
@@ -0,0 +1,88 @@
1
+
2
+ import { getVersion, getDependencyVersion, isFileAccessible } from '../utils.mjs'
3
+ import { createPackageJson } from '../create-package-json.mjs'
4
+ import { createGitignore } from '../create-gitignore.mjs'
5
+ import { getPkgManager } from '../get-pkg-manager.mjs'
6
+ import parseArgs from 'minimist'
7
+ import { join } from 'path'
8
+ import inquirer from 'inquirer'
9
+ import { readFile, writeFile } from 'fs/promises'
10
+ import pino from 'pino'
11
+ import pretty from 'pino-pretty'
12
+ import { execa } from 'execa'
13
+ import ora from 'ora'
14
+ import createService from './create-service.mjs'
15
+ import askProjectDir from '../ask-project-dir.mjs'
16
+ import { askCreateGHAction } from '../ghaction.mjs'
17
+ import mkdirp from 'mkdirp'
18
+
19
+ export const createReadme = async (logger, dir = '.') => {
20
+ const readmeFileName = join(dir, 'README.md')
21
+ const isReadmeExists = await isFileAccessible(readmeFileName)
22
+ if (!isReadmeExists) {
23
+ const readmeFile = new URL('README.md', import.meta.url)
24
+ const readme = await readFile(readmeFile, 'utf-8')
25
+ await writeFile(readmeFileName, readme)
26
+ logger.debug(`${readmeFileName} successfully created.`)
27
+ } else {
28
+ logger.debug(`${readmeFileName} found, skipping creation of README.md file.`)
29
+ }
30
+ }
31
+
32
+ const createPlatformaticService = async (_args) => {
33
+ const logger = pino(pretty({
34
+ translateTime: 'SYS:HH:MM:ss',
35
+ ignore: 'hostname,pid'
36
+ }))
37
+
38
+ const args = parseArgs(_args, {
39
+ default: {
40
+ hostname: '127.0.0.1',
41
+ port: 3042
42
+ },
43
+ alias: {
44
+ h: 'hostname',
45
+ p: 'port'
46
+ }
47
+ })
48
+
49
+ const version = await getVersion()
50
+ const pkgManager = getPkgManager()
51
+
52
+ const projectDir = await askProjectDir(logger, '.')
53
+
54
+ // Create the project directory
55
+ await mkdirp(projectDir)
56
+
57
+ const params = {
58
+ hostname: args.hostname,
59
+ port: args.port
60
+ }
61
+
62
+ const env = await createService(params, logger, projectDir)
63
+
64
+ const fastifyVersion = await getDependencyVersion('fastify')
65
+
66
+ // Create the package.json, .gitignore, readme
67
+ await createPackageJson('service', version, fastifyVersion, logger, projectDir)
68
+ await createGitignore(logger, projectDir)
69
+ await createReadme(logger, projectDir)
70
+
71
+ const { runPackageManagerInstall } = await inquirer.prompt([{
72
+ type: 'list',
73
+ name: 'runPackageManagerInstall',
74
+ message: `Do you want to run ${pkgManager} install?`,
75
+ default: true,
76
+ choices: [{ name: 'yes', value: true }, { name: 'no', value: false }]
77
+ }])
78
+
79
+ if (runPackageManagerInstall) {
80
+ const spinner = ora('Installing dependencies...').start()
81
+ await execa(pkgManager, ['install'], { cwd: projectDir })
82
+ spinner.succeed('...done!')
83
+ }
84
+
85
+ await askCreateGHAction(logger, env, 'service')
86
+ }
87
+
88
+ export default createPlatformaticService
@@ -0,0 +1,94 @@
1
+ import { writeFile, mkdir } from 'fs/promises'
2
+ import { join } from 'path'
3
+ import { findServiceConfigFile, isFileAccessible } from '../utils.mjs'
4
+
5
+ function generateConfig () {
6
+ const plugin = [
7
+ './plugins',
8
+ './routes'
9
+ ]
10
+
11
+ const config = {
12
+ server: {
13
+ hostname: '{PLT_SERVER_HOSTNAME}',
14
+ port: '{PORT}',
15
+ logger: {
16
+ level: '{PLT_SERVER_LOGGER_LEVEL}'
17
+ }
18
+ },
19
+ plugin
20
+ }
21
+
22
+ return config
23
+ }
24
+
25
+ function generateEnv (hostname, port) {
26
+ const env = `\
27
+ PLT_SERVER_HOSTNAME=${hostname}
28
+ PORT=${port}
29
+ PLT_SERVER_LOGGER_LEVEL=info
30
+ `
31
+ return env
32
+ }
33
+
34
+ const JS_PLUGIN_WITH_TYPES_SUPPORT = `\
35
+ 'use strict'
36
+ /** @param {import('fastify').FastifyInstance} fastify */
37
+ module.exports = async function (fastify, opts) {
38
+ fastify.decorate('example', 'foobar')
39
+ }
40
+ module.exports[Symbol.for('skip-override')] = true
41
+ `
42
+
43
+ const ROUTES_WITH_TYPES_SUPPORT = `\
44
+ 'use strict'
45
+ /** @param {import('fastify').FastifyInstance} fastify */
46
+ module.exports = async function (fastify, opts) {
47
+ fastify.get('/', async (request, reply) => {
48
+ return { hello: fastify.example }
49
+ })
50
+ }
51
+ `
52
+
53
+ async function createService ({ hostname, port }, logger, currentDir = process.cwd()) {
54
+ const accessibleConfigFilename = await findServiceConfigFile(currentDir)
55
+
56
+ if (accessibleConfigFilename === undefined) {
57
+ const config = generateConfig()
58
+ await writeFile(join(currentDir, 'platformatic.service.json'), JSON.stringify(config, null, 2))
59
+ logger.info('Configuration file platformatic.service.json successfully created.')
60
+
61
+ const env = generateEnv(hostname, port)
62
+ await writeFile(join(currentDir, '.env'), env)
63
+ await writeFile(join(currentDir, '.env.sample'), env)
64
+ logger.info('Environment file .env successfully created.')
65
+ } else {
66
+ logger.info(`Configuration file ${accessibleConfigFilename} found, skipping creation of configuration file.`)
67
+ }
68
+
69
+ const pluginFolderExists = await isFileAccessible('plugins', currentDir)
70
+ if (!pluginFolderExists) {
71
+ await mkdir(join(currentDir, 'plugins'))
72
+ await writeFile(join(currentDir, 'plugins', 'example.js'), JS_PLUGIN_WITH_TYPES_SUPPORT)
73
+ logger.info('Plugins folder "plugins" successfully created.')
74
+ } else {
75
+ logger.info('Plugins folder "plugins" found, skipping creation of plugins folder.')
76
+ }
77
+
78
+ const routeFolderExists = await isFileAccessible('routes', currentDir)
79
+ if (!routeFolderExists) {
80
+ await mkdir(join(currentDir, 'routes'))
81
+ await writeFile(join(currentDir, 'routes', 'root.js'), ROUTES_WITH_TYPES_SUPPORT)
82
+ logger.info('Routes folder "routes" successfully created.')
83
+ } else {
84
+ logger.info('Routes folder "routes" found, skipping creation of routes folder.')
85
+ }
86
+
87
+ return {
88
+ PLT_SERVER_LOGGER_LEVEL: 'info',
89
+ PORT: port,
90
+ PLT_SERVER_HOSTNAME: hostname
91
+ }
92
+ }
93
+
94
+ export default createService
package/src/utils.mjs ADDED
@@ -0,0 +1,95 @@
1
+ import { execa } from 'execa'
2
+ import { request } from 'undici'
3
+ import { access, constants, readFile } from 'fs/promises'
4
+ import { resolve, join, dirname } from 'path'
5
+ import { createRequire } from 'module'
6
+
7
+ export const sleep = ms => new Promise((resolve) => setTimeout(resolve, ms))
8
+ export const randomBetween = (min, max) => Math.floor(Math.random() * (max - min + 1) + min)
9
+
10
+ export async function isFileAccessible (filename, directory) {
11
+ try {
12
+ const filePath = directory ? resolve(directory, filename) : filename
13
+ await access(filePath)
14
+ return true
15
+ } catch (err) {
16
+ return false
17
+ }
18
+ }
19
+
20
+ export const getUsername = async () => {
21
+ const { stdout } = await execa('git', ['config', 'user.name'])
22
+ if (stdout?.trim()) {
23
+ return stdout.trim()
24
+ }
25
+ {
26
+ const { stdout } = await execa('whoami')
27
+ if (stdout?.trim()) {
28
+ return stdout.trim()
29
+ }
30
+ }
31
+ return null
32
+ }
33
+
34
+ export const getVersion = async () => {
35
+ try {
36
+ const { body, statusCode } = await request('https://registry.npmjs.org/platformatic/latest')
37
+ if (statusCode !== 200) {
38
+ return null
39
+ }
40
+ const { version } = await body.json()
41
+ return version
42
+ } catch (err) {
43
+ return null
44
+ }
45
+ }
46
+
47
+ export async function isDirectoryWriteable (directory) {
48
+ try {
49
+ await access(directory, constants.R_OK | constants.W_OK)
50
+ return true
51
+ } catch (err) {
52
+ return false
53
+ }
54
+ }
55
+
56
+ export const validatePath = async projectPath => {
57
+ // if the folder exists, is OK:
58
+ const projectDir = resolve(projectPath)
59
+ const canAccess = await isDirectoryWriteable(projectDir)
60
+ if (canAccess) {
61
+ return true
62
+ }
63
+ // if the folder does not exist, check if the parent folder exists:
64
+ const parentDir = dirname(projectDir)
65
+ const canAccessParent = await isDirectoryWriteable(parentDir)
66
+ if (canAccessParent) {
67
+ return true
68
+ }
69
+ return false
70
+ }
71
+
72
+ const findConfigFile = async (directory, type) => {
73
+ const configFileNames = [
74
+ `platformatic.${type}.json`,
75
+ `platformatic.${type}.json5`,
76
+ `platformatic.${type}.yaml`,
77
+ `platformatic.${type}.yml`,
78
+ `platformatic.${type}.toml`,
79
+ `platformatic.${type}.tml`
80
+ ]
81
+ const configFilesAccessibility = await Promise.all(configFileNames.map((fileName) => isFileAccessible(fileName, directory)))
82
+ const accessibleConfigFilename = configFileNames.find((value, index) => configFilesAccessibility[index])
83
+ return accessibleConfigFilename
84
+ }
85
+
86
+ export const findDBConfigFile = async (directory) => (findConfigFile(directory, 'db'))
87
+ export const findServiceConfigFile = async (directory) => (findConfigFile(directory, 'service'))
88
+
89
+ export const getDependencyVersion = async (dependencyName) => {
90
+ const require = createRequire(import.meta.url)
91
+ const pathToPackageJson = join(dirname(require.resolve(dependencyName)), 'package.json')
92
+ const packageJsonFile = await readFile(pathToPackageJson, 'utf-8')
93
+ const packageJson = JSON.parse(packageJsonFile)
94
+ return packageJson.version
95
+ }