@wirechunk/cli 0.0.1-rc.1 → 0.0.1-rc.2
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/build/main.js +30447 -43303
- package/package.json +3 -3
- package/src/commands/create-extension-version.ts +36 -5
- package/src/commands/create-extension.ts +14 -0
- package/src/commands/ext-dev/db-connect-info.ts +36 -0
- package/src/commands/{dev/init-ext-db.ts → ext-dev/init-db.ts} +39 -78
- package/src/env.ts +1 -1
- package/src/main.ts +18 -6
- package/src/util.ts +52 -0
- package/src/fetch.ts +0 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirechunk/cli",
|
|
3
|
-
"version": "0.0.1-rc.
|
|
3
|
+
"version": "0.0.1-rc.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"test": "echo 'no tests yet'",
|
|
14
14
|
"build": "npm run build:clean && npm run build:cli",
|
|
15
15
|
"build:clean": "rm -rf build",
|
|
16
|
-
"build:cli": "vite build"
|
|
16
|
+
"build:cli": "vite build",
|
|
17
|
+
"prepublishOnly": "npm run typecheck-src && npm run lint:check && npm run build"
|
|
17
18
|
},
|
|
18
19
|
"bin": {
|
|
19
20
|
"wirechunk": "build/main.js"
|
|
@@ -31,7 +32,6 @@
|
|
|
31
32
|
"commander": "^13.0.0",
|
|
32
33
|
"slonik": "^46.3.0",
|
|
33
34
|
"slonik-interceptor-query-logging": "^46.3.0",
|
|
34
|
-
"undici": "^7.2.0",
|
|
35
35
|
"zod": "^3.24.1"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { requireValidExtensionDir } from '@wirechunk/backend-lib/extensions/require-extension-dir.ts';
|
|
1
2
|
import { parseErrorMessage } from '@wirechunk/lib/errors.ts';
|
|
2
3
|
import archiver from 'archiver';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
import type { Env } from '../env.ts';
|
|
5
6
|
import { requireApiToken } from '../env.ts';
|
|
6
|
-
import { fetchWithLocal } from '../fetch.ts';
|
|
7
7
|
import type { WithGlobalOptions } from '../global-options.js';
|
|
8
8
|
import { requireExtensionIdOptionOrEnvVar } from '../util.ts';
|
|
9
9
|
|
|
@@ -62,6 +62,35 @@ export const createExtensionVersion = async (
|
|
|
62
62
|
): Promise<void> => {
|
|
63
63
|
const extensionId = requireExtensionIdOptionOrEnvVar(opts, env);
|
|
64
64
|
const apiToken = requireApiToken(env);
|
|
65
|
+
const cwd = process.cwd();
|
|
66
|
+
const extConfig = await requireValidExtensionDir(cwd);
|
|
67
|
+
let enableServer: boolean;
|
|
68
|
+
let enableDb: boolean;
|
|
69
|
+
if (extConfig.server) {
|
|
70
|
+
enableServer = !!extConfig.server.enable;
|
|
71
|
+
if (extConfig.server.database?.enable && extConfig.server.enable === false) {
|
|
72
|
+
// Server was explicitly disabled, so don't allow database.
|
|
73
|
+
console.warn('WARNING: Automatically disabling database because server is disabled');
|
|
74
|
+
enableDb = false;
|
|
75
|
+
} else if (extConfig.server.database?.enable && !extConfig.server.enable) {
|
|
76
|
+
// Server was unspecified, so enable it because database is enabled.
|
|
77
|
+
console.warn('WARNING: Automatically enabling server because database is enabled');
|
|
78
|
+
enableServer = true;
|
|
79
|
+
enableDb = true;
|
|
80
|
+
} else {
|
|
81
|
+
enableDb = !!extConfig.server.database?.enable;
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
enableServer = false;
|
|
85
|
+
enableDb = false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { versionName } = opts;
|
|
89
|
+
console.log(`Creating extension version ${versionName} (Extension ID ${extensionId})
|
|
90
|
+
Server: ${enableServer ? 'enabled' : 'disabled'}
|
|
91
|
+
Database: ${enableDb ? 'enabled' : 'disabled'}
|
|
92
|
+
Components: ${Object.keys(extConfig.components ?? {}).length}`);
|
|
93
|
+
|
|
65
94
|
let extensionVersionId: string;
|
|
66
95
|
let signedUrl: string;
|
|
67
96
|
try {
|
|
@@ -69,7 +98,7 @@ export const createExtensionVersion = async (
|
|
|
69
98
|
if (opts.verbose) {
|
|
70
99
|
console.log(`POST ${url}`);
|
|
71
100
|
}
|
|
72
|
-
const createResult = await
|
|
101
|
+
const createResult = await fetch(url, {
|
|
73
102
|
method: 'POST',
|
|
74
103
|
headers: {
|
|
75
104
|
'Content-Type': 'application/json',
|
|
@@ -95,7 +124,10 @@ export const createExtensionVersion = async (
|
|
|
95
124
|
variables: {
|
|
96
125
|
input: {
|
|
97
126
|
extensionId,
|
|
98
|
-
|
|
127
|
+
extensionName: extConfig.name,
|
|
128
|
+
versionName,
|
|
129
|
+
enableServer,
|
|
130
|
+
enableDb,
|
|
99
131
|
},
|
|
100
132
|
},
|
|
101
133
|
}),
|
|
@@ -163,7 +195,6 @@ export const createExtensionVersion = async (
|
|
|
163
195
|
});
|
|
164
196
|
}
|
|
165
197
|
|
|
166
|
-
const cwd = process.cwd();
|
|
167
198
|
archive.glob('**/*', {
|
|
168
199
|
cwd,
|
|
169
200
|
ignore: [
|
|
@@ -202,5 +233,5 @@ export const createExtensionVersion = async (
|
|
|
202
233
|
console.log('Uploaded');
|
|
203
234
|
}
|
|
204
235
|
|
|
205
|
-
console.log(`Created version ${
|
|
236
|
+
console.log(`Created version ${versionName} (ID ${extensionVersionId})`);
|
|
206
237
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Env } from '../env.ts';
|
|
2
|
+
import type { WithGlobalOptions } from '../global-options.ts';
|
|
3
|
+
|
|
4
|
+
type CreateExtensionOptions = {
|
|
5
|
+
platformId: string;
|
|
6
|
+
name: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const createExtension = async (
|
|
10
|
+
_opts: WithGlobalOptions<CreateExtensionOptions>,
|
|
11
|
+
_env: Env,
|
|
12
|
+
): Promise<void> => {
|
|
13
|
+
// TODO
|
|
14
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import type { Env } from '../../env.ts';
|
|
5
|
+
import type { WithGlobalOptions } from '../../global-options.js';
|
|
6
|
+
import { getExtensionDbConnectInfo, replaceEnvVar } from '../../util.ts';
|
|
7
|
+
|
|
8
|
+
type DbConnectInfoOptions = {
|
|
9
|
+
extensionId?: string;
|
|
10
|
+
dbName?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const dbConnectInfo = async (
|
|
14
|
+
opts: WithGlobalOptions<DbConnectInfoOptions>,
|
|
15
|
+
env: Env,
|
|
16
|
+
): Promise<void> => {
|
|
17
|
+
const connInfo = getExtensionDbConnectInfo(opts, env);
|
|
18
|
+
const url = connInfo.url.toString();
|
|
19
|
+
const cwd = process.cwd();
|
|
20
|
+
const localEnvFilePath = opts.envMode
|
|
21
|
+
? resolve(cwd, `./.env.${opts.envMode}.local`)
|
|
22
|
+
: resolve(cwd, './.env.local');
|
|
23
|
+
if (opts.verbose) {
|
|
24
|
+
console.log(`Loading file ${localEnvFilePath} to update environment variables`);
|
|
25
|
+
}
|
|
26
|
+
if (existsSync(localEnvFilePath)) {
|
|
27
|
+
const localEnv = await readFile(localEnvFilePath, 'utf8');
|
|
28
|
+
const lines = replaceEnvVar(localEnv.split('\n'), 'DATABASE_URL', url);
|
|
29
|
+
await writeFile(localEnvFilePath, lines.join('\n'));
|
|
30
|
+
} else {
|
|
31
|
+
if (opts.verbose) {
|
|
32
|
+
console.log(`Creating file ${localEnvFilePath}`);
|
|
33
|
+
}
|
|
34
|
+
await writeFile(localEnvFilePath, `DATABASE_URL=${url}\n`);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
4
1
|
import type { DatabasePool } from 'slonik';
|
|
5
2
|
import { createPool, sql } from 'slonik';
|
|
6
3
|
import { z } from 'zod';
|
|
@@ -8,18 +5,22 @@ import type { Env } from '../../env.js';
|
|
|
8
5
|
import { requireCoreDbUrl } from '../../env.ts';
|
|
9
6
|
import { isDuplicateDatabaseError } from '../../errors.ts';
|
|
10
7
|
import type { WithGlobalOptions } from '../../global-options.js';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
import {
|
|
9
|
+
dbPoolOptions,
|
|
10
|
+
getExtensionDbConnectInfo,
|
|
11
|
+
requireExtensionIdOptionOrEnvVar,
|
|
12
|
+
} from '../../util.ts';
|
|
13
|
+
import { dbConnectInfo } from './db-connect-info.ts';
|
|
14
|
+
|
|
15
|
+
const initExtDb = async ({
|
|
16
|
+
// A connection to the extension database by a superuser role.
|
|
17
|
+
db,
|
|
15
18
|
extensionRoleName,
|
|
16
19
|
coreDbUrl,
|
|
17
|
-
db,
|
|
18
20
|
}: {
|
|
19
|
-
|
|
21
|
+
db: DatabasePool;
|
|
20
22
|
extensionRoleName: string;
|
|
21
23
|
coreDbUrl: URL;
|
|
22
|
-
db: DatabasePool;
|
|
23
24
|
}) => {
|
|
24
25
|
await db.query(sql.unsafe`
|
|
25
26
|
create extension if not exists postgres_fdw
|
|
@@ -34,10 +35,10 @@ const initSchemas = async ({
|
|
|
34
35
|
)
|
|
35
36
|
`);
|
|
36
37
|
|
|
37
|
-
const
|
|
38
|
+
const extRoleIdent = sql.identifier([extensionRoleName]);
|
|
38
39
|
|
|
39
40
|
await db.query(sql.unsafe`
|
|
40
|
-
create user mapping if not exists for ${
|
|
41
|
+
create user mapping if not exists for ${extRoleIdent} server wirechunk
|
|
41
42
|
options (user ${sql.literalValue(extensionRoleName)}, password_required 'false')
|
|
42
43
|
`);
|
|
43
44
|
await db.query(sql.unsafe`
|
|
@@ -76,69 +77,39 @@ const initSchemas = async ({
|
|
|
76
77
|
// Here we just want to simplify the development experience.
|
|
77
78
|
// At any moment, a developer may drop an extension's database and reinitialize it.
|
|
78
79
|
await db.query(sql.unsafe`
|
|
79
|
-
grant
|
|
80
|
-
`);
|
|
81
|
-
await db.query(sql.unsafe`
|
|
82
|
-
grant all on schema public to ${extRoleNameIdent}
|
|
80
|
+
grant all on schema public to ${extRoleIdent}
|
|
83
81
|
`);
|
|
84
82
|
await db.query(sql.unsafe`
|
|
85
|
-
grant all on table "Orgs" to ${
|
|
83
|
+
grant all on table "Orgs" to ${extRoleIdent}
|
|
86
84
|
`);
|
|
87
85
|
await db.query(sql.unsafe`
|
|
88
|
-
grant all on table "Sites" to ${
|
|
86
|
+
grant all on table "Sites" to ${extRoleIdent}
|
|
89
87
|
`);
|
|
90
88
|
await db.query(sql.unsafe`
|
|
91
|
-
grant all on table "Users" to ${
|
|
89
|
+
grant all on table "Users" to ${extRoleIdent}
|
|
92
90
|
`);
|
|
93
91
|
await db.query(sql.unsafe`
|
|
94
|
-
alter default privileges grant all on schemas to ${
|
|
92
|
+
alter default privileges grant all on schemas to ${extRoleIdent}
|
|
95
93
|
`);
|
|
96
94
|
await db.query(sql.unsafe`
|
|
97
|
-
alter default privileges grant all on types to ${
|
|
95
|
+
alter default privileges grant all on types to ${extRoleIdent}
|
|
98
96
|
`);
|
|
99
97
|
await db.query(sql.unsafe`
|
|
100
|
-
alter default privileges grant all on tables to ${
|
|
98
|
+
alter default privileges grant all on tables to ${extRoleIdent}
|
|
101
99
|
`);
|
|
102
100
|
await db.query(sql.unsafe`
|
|
103
|
-
alter default privileges grant all on sequences to ${
|
|
101
|
+
alter default privileges grant all on sequences to ${extRoleIdent}
|
|
104
102
|
`);
|
|
105
103
|
await db.query(sql.unsafe`
|
|
106
|
-
alter default privileges grant all on functions to ${
|
|
104
|
+
alter default privileges grant all on functions to ${extRoleIdent}
|
|
107
105
|
`);
|
|
108
106
|
};
|
|
109
107
|
|
|
110
|
-
const replaceDbName = (url: URL, dbName: string): URL => {
|
|
111
|
-
const urlObj = new URL(url.toString());
|
|
112
|
-
urlObj.pathname = `/${dbName}`;
|
|
113
|
-
return urlObj;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
108
|
const extensionSelectSchema = z.object({
|
|
117
109
|
id: z.string(),
|
|
118
110
|
platformId: z.string(),
|
|
119
111
|
});
|
|
120
112
|
|
|
121
|
-
// Replace instances of a variable, or add a variable if needed, preserving other lines.
|
|
122
|
-
const replaceEnvVar = (lines: string[], name: string, value: string): string[] => {
|
|
123
|
-
const newLines: string[] = [];
|
|
124
|
-
let hasName = false;
|
|
125
|
-
const namePattern = new RegExp(String.raw`^\s*${name}\s*=\s*`);
|
|
126
|
-
for (const line of lines) {
|
|
127
|
-
if (namePattern.test(line)) {
|
|
128
|
-
if (!hasName) {
|
|
129
|
-
newLines.push(`${name}=${value}`);
|
|
130
|
-
hasName = true;
|
|
131
|
-
}
|
|
132
|
-
} else {
|
|
133
|
-
newLines.push(line);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if (!hasName) {
|
|
137
|
-
newLines.push(`${name}=${value}`);
|
|
138
|
-
}
|
|
139
|
-
return newLines;
|
|
140
|
-
};
|
|
141
|
-
|
|
142
113
|
const applyExtensionPolicy = async ({
|
|
143
114
|
extRole,
|
|
144
115
|
table,
|
|
@@ -150,7 +121,7 @@ const applyExtensionPolicy = async ({
|
|
|
150
121
|
platformId: string;
|
|
151
122
|
db: DatabasePool;
|
|
152
123
|
}) => {
|
|
153
|
-
const policyNameIdent = sql.identifier([`${table}
|
|
124
|
+
const policyNameIdent = sql.identifier([`${table}_${extRole}_select`]);
|
|
154
125
|
const extRoleIdent = sql.identifier([extRole]);
|
|
155
126
|
const tableIdent = sql.identifier([table]);
|
|
156
127
|
await db.query(sql.unsafe`
|
|
@@ -162,7 +133,7 @@ const applyExtensionPolicy = async ({
|
|
|
162
133
|
using ("platformId" = ${sql.literalValue(platformId)})
|
|
163
134
|
`);
|
|
164
135
|
await db.query(sql.unsafe`
|
|
165
|
-
grant select on ${tableIdent} to ${extRoleIdent}
|
|
136
|
+
grant select on table ${tableIdent} to ${extRoleIdent}
|
|
166
137
|
`);
|
|
167
138
|
};
|
|
168
139
|
|
|
@@ -171,12 +142,15 @@ type InitExtDbOptions = {
|
|
|
171
142
|
dbName?: string;
|
|
172
143
|
};
|
|
173
144
|
|
|
174
|
-
export const
|
|
145
|
+
export const initDb = async (
|
|
175
146
|
opts: WithGlobalOptions<InitExtDbOptions>,
|
|
176
147
|
env: Env,
|
|
177
148
|
): Promise<void> => {
|
|
178
149
|
const coreDbUrl = requireCoreDbUrl(env);
|
|
179
150
|
const extensionId = requireExtensionIdOptionOrEnvVar(opts, env);
|
|
151
|
+
|
|
152
|
+
await dbConnectInfo(opts, env);
|
|
153
|
+
|
|
180
154
|
const db = await createPool(coreDbUrl, dbPoolOptions(opts));
|
|
181
155
|
|
|
182
156
|
const extension = await db.maybeOne(sql.type(extensionSelectSchema)`
|
|
@@ -189,30 +163,12 @@ export const initExtDb = async (
|
|
|
189
163
|
process.exit(1);
|
|
190
164
|
}
|
|
191
165
|
|
|
192
|
-
const extDb = opts.dbName || extensionDbName(extensionId);
|
|
193
|
-
|
|
194
166
|
const coreDbUrlObject = new URL(coreDbUrl);
|
|
195
|
-
const
|
|
196
|
-
extDbUrl.pathname = `/${extDb}`;
|
|
197
|
-
|
|
198
|
-
const cwd = process.cwd();
|
|
199
|
-
const localEnvFilePath = opts.envMode
|
|
200
|
-
? resolve(cwd, `./.env.${opts.envMode}.local`)
|
|
201
|
-
: resolve(cwd, './.env.local');
|
|
202
|
-
if (opts.verbose) {
|
|
203
|
-
console.log(`Loading ${localEnvFilePath} to update environment variables`);
|
|
204
|
-
}
|
|
205
|
-
if (existsSync(localEnvFilePath)) {
|
|
206
|
-
const localEnv = await readFile(localEnvFilePath, 'utf8');
|
|
207
|
-
const lines = replaceEnvVar(localEnv.split('\n'), 'DATABASE_URL', extDbUrl.toString());
|
|
208
|
-
await writeFile(localEnvFilePath, lines.join('\n'));
|
|
209
|
-
} else {
|
|
210
|
-
await writeFile(localEnvFilePath, `DATABASE_URL=${extDbUrl.toString()}\n`);
|
|
211
|
-
}
|
|
167
|
+
const extDb = getExtensionDbConnectInfo(opts, env);
|
|
212
168
|
|
|
213
169
|
try {
|
|
214
170
|
await db.query(sql.unsafe`
|
|
215
|
-
create database ${sql.identifier([extDb])}
|
|
171
|
+
create database ${sql.identifier([extDb.dbName])}
|
|
216
172
|
`);
|
|
217
173
|
} catch (err) {
|
|
218
174
|
if (!isDuplicateDatabaseError(err)) {
|
|
@@ -230,11 +186,14 @@ export const initExtDb = async (
|
|
|
230
186
|
if not exists (
|
|
231
187
|
select 1 from pg_roles where rolname = ${sql.literalValue(extRole)}
|
|
232
188
|
) then
|
|
233
|
-
create role ${extRoleIdent} login noinherit;
|
|
189
|
+
create role ${extRoleIdent} login noinherit createdb;
|
|
234
190
|
end if;
|
|
235
191
|
end; $$
|
|
236
192
|
`);
|
|
237
193
|
|
|
194
|
+
await db.query(sql.unsafe`
|
|
195
|
+
grant connect, create, temporary on database ${sql.identifier([extDb.dbName])} to ${extRoleIdent}
|
|
196
|
+
`);
|
|
238
197
|
await applyExtensionPolicy({
|
|
239
198
|
extRole,
|
|
240
199
|
table: 'Orgs',
|
|
@@ -254,10 +213,12 @@ export const initExtDb = async (
|
|
|
254
213
|
db,
|
|
255
214
|
});
|
|
256
215
|
|
|
257
|
-
|
|
258
|
-
|
|
216
|
+
const extDbSuper = new URL(extDb.url);
|
|
217
|
+
extDbSuper.username = coreDbUrlObject.username;
|
|
218
|
+
|
|
219
|
+
await initExtDb({
|
|
220
|
+
db: await createPool(extDbSuper.toString(), dbPoolOptions(opts)),
|
|
259
221
|
extensionRoleName: extRole,
|
|
260
222
|
coreDbUrl: coreDbUrlObject,
|
|
261
|
-
db: await createPool(extDbUrl.toString(), dbPoolOptions(opts)),
|
|
262
223
|
});
|
|
263
224
|
};
|
package/src/env.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { isLocalhost } from '@wirechunk/lib/localhost.ts';
|
|
|
6
6
|
|
|
7
7
|
export type Env = Record<string, string> & {
|
|
8
8
|
CORE_DATABASE_URL?: string;
|
|
9
|
-
// A URL to the server root, like https://wirechunk.com or http://
|
|
9
|
+
// A URL to the server root, like https://wirechunk.com or http://localhost:8080
|
|
10
10
|
CORE_SERVER_URL: string;
|
|
11
11
|
WIRECHUNK_API_TOKEN?: string;
|
|
12
12
|
};
|
package/src/main.ts
CHANGED
|
@@ -6,8 +6,9 @@ import chalk from 'chalk';
|
|
|
6
6
|
import { bootstrap } from './commands/bootstrap.ts';
|
|
7
7
|
import { createExtensionVersion } from './commands/create-extension-version.ts';
|
|
8
8
|
import { createUser } from './commands/create-user.ts';
|
|
9
|
-
import { initExtDb } from './commands/dev/init-ext-db.ts';
|
|
10
9
|
import { editAdmin } from './commands/edit-admin.ts';
|
|
10
|
+
import { dbConnectInfo } from './commands/ext-dev/db-connect-info.ts';
|
|
11
|
+
import { initDb } from './commands/ext-dev/init-db.ts';
|
|
11
12
|
import type { Env } from './env.ts';
|
|
12
13
|
import { parseEnv } from './env.ts';
|
|
13
14
|
import type { WithGlobalOptions } from './global-options.ts';
|
|
@@ -90,17 +91,28 @@ program
|
|
|
90
91
|
|
|
91
92
|
// TODO: create-admin
|
|
92
93
|
|
|
93
|
-
const
|
|
94
|
+
const extDev = program.command('ext-dev').description('extension development commands');
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
.command('
|
|
97
|
-
.description(
|
|
96
|
+
extDev
|
|
97
|
+
.command('db-connect-info')
|
|
98
|
+
.description(
|
|
99
|
+
'write the connection info for the extension database to a .local.env or .local.<env-mode>.env file',
|
|
100
|
+
)
|
|
101
|
+
.option(
|
|
102
|
+
'--extension-id <string>',
|
|
103
|
+
'the ID of the extension, can be set with an EXTENSION_ID environment variable instead',
|
|
104
|
+
)
|
|
105
|
+
.action(withOptionsAndEnv(dbConnectInfo));
|
|
106
|
+
|
|
107
|
+
extDev
|
|
108
|
+
.command('init-db')
|
|
109
|
+
.description('initialize a development database for an extension, useful for testing')
|
|
98
110
|
.option(
|
|
99
111
|
'--extension-id <string>',
|
|
100
112
|
'the ID of the extension, can be set with an EXTENSION_ID environment variable instead',
|
|
101
113
|
)
|
|
102
114
|
.option('--db-name <string>', 'a custom name for the database, applicable only for testing')
|
|
103
|
-
.action(withOptionsAndEnv(
|
|
115
|
+
.action(withOptionsAndEnv(initDb));
|
|
104
116
|
|
|
105
117
|
program
|
|
106
118
|
.command('edit-admin')
|
package/src/util.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ClientConfigurationInput } from 'slonik/src/types.js';
|
|
|
2
2
|
import { createQueryLoggingInterceptor } from 'slonik-interceptor-query-logging';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import type { Env } from './env.ts';
|
|
5
|
+
import { requireCoreDbUrl } from './env.ts';
|
|
5
6
|
import type { GlobalOptions } from './global-options.ts';
|
|
6
7
|
|
|
7
8
|
// Returns the name to use for an extension's database in development mode.
|
|
@@ -27,3 +28,54 @@ export const requireExtensionIdOptionOrEnvVar = (
|
|
|
27
28
|
// Note that you still need to set a ROARR_LOG=true environment variable to enable logging.
|
|
28
29
|
export const dbPoolOptions = (opts: GlobalOptions): ClientConfigurationInput =>
|
|
29
30
|
opts.verbose ? { interceptors: [createQueryLoggingInterceptor({ logValues: true })] } : {};
|
|
31
|
+
|
|
32
|
+
// Replace instances of a variable, or add a variable if needed, preserving other lines.
|
|
33
|
+
export const replaceEnvVar = (lines: string[], name: string, value: string): string[] => {
|
|
34
|
+
const newLines: string[] = [];
|
|
35
|
+
let hasName = false;
|
|
36
|
+
const namePattern = new RegExp(String.raw`^\s*${name}\s*=\s*`);
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
if (namePattern.test(line)) {
|
|
39
|
+
if (!hasName) {
|
|
40
|
+
newLines.push(`${name}=${value}`);
|
|
41
|
+
hasName = true;
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
newLines.push(line);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!hasName) {
|
|
48
|
+
newLines.push(`${name}=${value}`);
|
|
49
|
+
}
|
|
50
|
+
return newLines;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const replaceDbName = (url: URL, dbName: string): URL => {
|
|
54
|
+
const urlObj = new URL(url.toString());
|
|
55
|
+
urlObj.pathname = `/${dbName}`;
|
|
56
|
+
return urlObj;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type DbConnectInfo = {
|
|
60
|
+
url: URL;
|
|
61
|
+
dbName: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const getExtensionDbConnectInfo = (
|
|
65
|
+
opts: {
|
|
66
|
+
extensionId?: string;
|
|
67
|
+
dbName?: string;
|
|
68
|
+
},
|
|
69
|
+
env: Env,
|
|
70
|
+
): DbConnectInfo => {
|
|
71
|
+
const extensionId = requireExtensionIdOptionOrEnvVar(opts, env);
|
|
72
|
+
const dbName = opts.dbName || extensionDbName(extensionId);
|
|
73
|
+
const url = replaceDbName(new URL(requireCoreDbUrl(env)), dbName);
|
|
74
|
+
url.username = `ext_${extensionId}`;
|
|
75
|
+
url.password = '';
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
url,
|
|
79
|
+
dbName,
|
|
80
|
+
};
|
|
81
|
+
};
|
package/src/fetch.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { lookup } from 'node:dns';
|
|
2
|
-
import { isLocalhost } from '@wirechunk/lib/localhost.ts';
|
|
3
|
-
import * as undici from 'undici';
|
|
4
|
-
|
|
5
|
-
// To allow easily accessing localhost subdomains without needing to override /etc/hosts, this function provides a DNS resolver
|
|
6
|
-
// that resolves to 127.0.0.1 for localhost domains.
|
|
7
|
-
export const fetchWithLocal = (
|
|
8
|
-
url: string,
|
|
9
|
-
init: RequestInit & undici.RequestInit,
|
|
10
|
-
): Promise<Response | undici.Response> => {
|
|
11
|
-
const { host } = new URL(url);
|
|
12
|
-
if (isLocalhost(host)) {
|
|
13
|
-
return undici.fetch(url, {
|
|
14
|
-
...init,
|
|
15
|
-
dispatcher: new undici.Agent({
|
|
16
|
-
connect: {
|
|
17
|
-
lookup: (hostname, options, callback) => {
|
|
18
|
-
if (isLocalhost(hostname)) {
|
|
19
|
-
callback(null, [
|
|
20
|
-
{ address: '127.0.0.1', family: 4 },
|
|
21
|
-
{ address: '::1', family: 6 },
|
|
22
|
-
]);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
lookup(hostname, options, callback);
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
}),
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
return fetch(url, init);
|
|
32
|
-
};
|