@wirechunk/cli 0.0.1-rc.1 → 0.0.1-rc.3
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 +30443 -43298
- 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/create-user.ts +12 -12
- package/src/commands/ext-dev/db-connect-info.ts +36 -0
- package/src/commands/{dev/init-ext-db.ts → ext-dev/init-db.ts} +42 -78
- package/src/env.ts +1 -1
- package/src/main.ts +20 -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.3",
|
|
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
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hashPassword, validatePasswordComplexity } from '@wirechunk/backend-lib/passwords.ts';
|
|
2
|
+
import { cleanSmallId } from '@wirechunk/lib/clean-small-id.ts';
|
|
2
3
|
import { normalizeEmailAddress } from '@wirechunk/lib/emails.ts';
|
|
3
4
|
import { createPool, sql, UniqueIntegrityConstraintViolationError } from 'slonik';
|
|
4
5
|
import { z } from 'zod';
|
|
@@ -6,10 +7,7 @@ import type { Env } from '../env.ts';
|
|
|
6
7
|
import { requireCoreDbUrl } from '../env.ts';
|
|
7
8
|
import { detailedUniqueIntegrityConstraintViolationError } from '../errors.ts';
|
|
8
9
|
import type { WithGlobalOptions } from '../global-options.ts';
|
|
9
|
-
|
|
10
|
-
const insertOrgResult = z.object({
|
|
11
|
-
id: z.string(),
|
|
12
|
-
});
|
|
10
|
+
import { voidSelectSchema } from '../util.ts';
|
|
13
11
|
|
|
14
12
|
const insertUserResult = z.object({
|
|
15
13
|
id: z.string(),
|
|
@@ -69,7 +67,8 @@ export const createUser = async (
|
|
|
69
67
|
|
|
70
68
|
let platformId: string | null | undefined = opts.platformId;
|
|
71
69
|
let orgId: string | null | undefined = opts.orgId;
|
|
72
|
-
|
|
70
|
+
// TODO
|
|
71
|
+
// let orgPrimary = false;
|
|
73
72
|
|
|
74
73
|
try {
|
|
75
74
|
const user = await db.transaction(async (db) => {
|
|
@@ -78,7 +77,7 @@ export const createUser = async (
|
|
|
78
77
|
throw new Error('Either --org-id or --platform-id must be specified');
|
|
79
78
|
}
|
|
80
79
|
platformId = await db.maybeOneFirst(
|
|
81
|
-
sql.type(findOrgResult)`select "platformId" from "
|
|
80
|
+
sql.type(findOrgResult)`select "platformId" from "Orgs" where "id" = ${orgId}`,
|
|
82
81
|
);
|
|
83
82
|
if (!platformId) {
|
|
84
83
|
throw new Error(`No org found with ID ${orgId}`);
|
|
@@ -86,7 +85,7 @@ export const createUser = async (
|
|
|
86
85
|
} else if (orgId) {
|
|
87
86
|
// Verify that the specified org belongs to the specified platform.
|
|
88
87
|
const orgPlatformId = await db.maybeOneFirst(
|
|
89
|
-
sql.type(findOrgResult)`select "platformId" from "
|
|
88
|
+
sql.type(findOrgResult)`select "platformId" from "Orgs" where "id" = ${orgId}`,
|
|
90
89
|
);
|
|
91
90
|
if (!orgPlatformId) {
|
|
92
91
|
throw new Error(`No org found with ID ${orgId}`);
|
|
@@ -107,21 +106,22 @@ export const createUser = async (
|
|
|
107
106
|
console.log(`Found platform ${platform.name} (ID ${platform.id})`);
|
|
108
107
|
}
|
|
109
108
|
if (!orgId) {
|
|
110
|
-
orgId =
|
|
109
|
+
orgId = cleanSmallId();
|
|
110
|
+
await db.maybeOne(
|
|
111
111
|
sql.type(
|
|
112
|
-
|
|
113
|
-
)`insert into "
|
|
112
|
+
voidSelectSchema,
|
|
113
|
+
)`insert into "Orgs" ("id", "platformId") values (${orgId}, ${platform.id})`,
|
|
114
114
|
);
|
|
115
115
|
if (opts.verbose) {
|
|
116
116
|
console.log(`Created org ID ${orgId}`);
|
|
117
117
|
}
|
|
118
|
-
orgPrimary = true;
|
|
118
|
+
// orgPrimary = true;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
const user = await db.one(
|
|
122
122
|
sql.type(
|
|
123
123
|
insertUserResult,
|
|
124
|
-
)`insert into "Users" ("platformId", "email", "emailVerified", "password", "passwordStatus", "orgId", "
|
|
124
|
+
)`insert into "Users" ("platformId", "email", "emailVerified", "password", "passwordStatus", "orgId", "role", "status", "firstName", "lastName") values (${platformId}, ${email}, ${opts.emailVerified}, ${password}, 'Ok', ${orgId}, ${role}, ${status}, ${firstName}, ${lastName}) returning "id"`,
|
|
125
125
|
);
|
|
126
126
|
|
|
127
127
|
// if (opts.admin) {
|
|
@@ -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,17 @@ 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
|
`);
|
|
193
|
+
await db.query(sql.unsafe`
|
|
194
|
+
grant ${extRoleIdent} to ${sql.identifier([coreDbUrlObject.username])}
|
|
195
|
+
`);
|
|
237
196
|
|
|
197
|
+
await db.query(sql.unsafe`
|
|
198
|
+
grant connect, create, temporary on database ${sql.identifier([extDb.dbName])} to ${extRoleIdent}
|
|
199
|
+
`);
|
|
238
200
|
await applyExtensionPolicy({
|
|
239
201
|
extRole,
|
|
240
202
|
table: 'Orgs',
|
|
@@ -254,10 +216,12 @@ export const initExtDb = async (
|
|
|
254
216
|
db,
|
|
255
217
|
});
|
|
256
218
|
|
|
257
|
-
|
|
258
|
-
|
|
219
|
+
const extDbSuper = new URL(extDb.url);
|
|
220
|
+
extDbSuper.username = coreDbUrlObject.username;
|
|
221
|
+
|
|
222
|
+
await initExtDb({
|
|
223
|
+
db: await createPool(extDbSuper.toString(), dbPoolOptions(opts)),
|
|
259
224
|
extensionRoleName: extRole,
|
|
260
225
|
coreDbUrl: coreDbUrlObject,
|
|
261
|
-
db: await createPool(extDbUrl.toString(), dbPoolOptions(opts)),
|
|
262
226
|
});
|
|
263
227
|
};
|
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,30 @@ 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(
|
|
110
|
+
'initialize a development database for an extension, useful for testing, assumes the extension role exists',
|
|
111
|
+
)
|
|
98
112
|
.option(
|
|
99
113
|
'--extension-id <string>',
|
|
100
114
|
'the ID of the extension, can be set with an EXTENSION_ID environment variable instead',
|
|
101
115
|
)
|
|
102
116
|
.option('--db-name <string>', 'a custom name for the database, applicable only for testing')
|
|
103
|
-
.action(withOptionsAndEnv(
|
|
117
|
+
.action(withOptionsAndEnv(initDb));
|
|
104
118
|
|
|
105
119
|
program
|
|
106
120
|
.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
|
-
};
|