@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirechunk/cli",
3
- "version": "0.0.1-rc.1",
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 fetchWithLocal(url, {
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
- versionName: opts.versionName,
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 ${opts.versionName} (ID ${extensionVersionId})`);
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
- let orgPrimary = false;
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 "Organizations" where "id" = ${orgId}`,
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 "Organizations" where "id" = ${orgId}`,
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 = await db.oneFirst(
109
+ orgId = cleanSmallId();
110
+ await db.maybeOne(
111
111
  sql.type(
112
- insertOrgResult,
113
- )`insert into "Organizations" ("platformId") values (${platform.id}) returning "id"`,
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", "orgPrimary", "role", "status", "firstName", "lastName") values (${platformId}, ${email}, ${opts.emailVerified}, ${password}, 'Ok', ${orgId}, ${orgPrimary}, ${role}, ${status}, ${firstName}, ${lastName}) returning "id"`,
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 { dbPoolOptions, extensionDbName, requireExtensionIdOptionOrEnvVar } from '../../util.ts';
12
-
13
- const initSchemas = async ({
14
- extensionDbName,
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
- extensionDbName: string;
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 extRoleNameIdent = sql.identifier([extensionRoleName]);
38
+ const extRoleIdent = sql.identifier([extensionRoleName]);
38
39
 
39
40
  await db.query(sql.unsafe`
40
- create user mapping if not exists for ${extRoleNameIdent} server wirechunk
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 connect, create, temporary on database ${sql.identifier([extensionDbName])} to ${extRoleNameIdent}
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 ${extRoleNameIdent}
83
+ grant all on table "Orgs" to ${extRoleIdent}
86
84
  `);
87
85
  await db.query(sql.unsafe`
88
- grant all on table "Sites" to ${extRoleNameIdent}
86
+ grant all on table "Sites" to ${extRoleIdent}
89
87
  `);
90
88
  await db.query(sql.unsafe`
91
- grant all on table "Users" to ${extRoleNameIdent}
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 ${extRoleNameIdent}
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 ${extRoleNameIdent}
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 ${extRoleNameIdent}
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 ${extRoleNameIdent}
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 ${extRoleNameIdent}
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}_ext_${extRole}_select`]);
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 initExtDb = async (
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 extDbUrl = replaceDbName(coreDbUrlObject, extDb);
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
- await initSchemas({
258
- extensionDbName: extDb,
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://admin.localhost:8080
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 dev = program.command('dev').description('extension development commands');
94
+ const extDev = program.command('ext-dev').description('extension development commands');
94
95
 
95
- dev
96
- .command('init-ext-db')
97
- .description('initialize a development database for an extension')
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(initExtDb));
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
- };