@wirechunk/cli 0.0.1-rc.3 → 0.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirechunk/cli",
3
- "version": "0.0.1-rc.3",
3
+ "version": "0.0.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -19,17 +19,22 @@
19
19
  "bin": {
20
20
  "wirechunk": "build/main.js"
21
21
  },
22
+ "imports": {
23
+ "#api": "./src/core-api/api.ts"
24
+ },
22
25
  "dependencies": {
23
26
  "argon2": "^0.41.1"
24
27
  },
25
28
  "devDependencies": {
26
29
  "@commander-js/extra-typings": "^13.0.0",
30
+ "@graphql-typed-document-node/core": "^3.2.0",
27
31
  "@types/archiver": "^6.0.3",
28
32
  "@wirechunk/backend-lib": "0.0.0",
29
33
  "@wirechunk/lib": "0.0.0",
30
34
  "archiver": "^7.0.1",
31
35
  "chalk": "^5.3.0",
32
36
  "commander": "^13.0.0",
37
+ "graphql-request": "^7.1.2",
33
38
  "slonik": "^46.3.0",
34
39
  "slonik-interceptor-query-logging": "^46.3.0",
35
40
  "zod": "^3.24.1"
@@ -1,11 +1,11 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { cleanTinyId } from '@wirechunk/lib/clean-small-id.ts';
3
+ import { normalizeDomain } from '@wirechunk/lib/domains.ts';
3
4
  import { normalizeEmailAddress } from '@wirechunk/lib/emails.ts';
4
5
  import {
5
6
  defaultFormattedDataTemplate,
6
7
  defaultNotificationEmailBodyTemplate,
7
8
  } from '@wirechunk/lib/mixer/form-formatting-templates.ts';
8
- import { normalizeDomain } from '@wirechunk/server/site-domains/util.ts';
9
9
  import type { DatabasePool } from 'slonik';
10
10
  import { createPool, sql } from 'slonik';
11
11
  import type { Env } from '../env.ts';
@@ -1,7 +1,11 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { parseEnv } from 'node:util';
1
4
  import { requireValidExtensionDir } from '@wirechunk/backend-lib/extensions/require-extension-dir.ts';
2
5
  import { parseErrorMessage } from '@wirechunk/lib/errors.ts';
3
6
  import archiver from 'archiver';
4
- import { z } from 'zod';
7
+ import { GraphQLClient } from 'graphql-request';
8
+ import { createExtensionVersion as createExtensionVersionRequest } from '../core-api/operations.ts';
5
9
  import type { Env } from '../env.ts';
6
10
  import { requireApiToken } from '../env.ts';
7
11
  import type { WithGlobalOptions } from '../global-options.js';
@@ -16,44 +20,21 @@ const bytesFormat = Intl.NumberFormat('en', {
16
20
  maximumFractionDigits: 1,
17
21
  });
18
22
 
19
- const createExtensionVersionResultDataSchema = z.discriminatedUnion('__typename', [
20
- z.object({
21
- __typename: z.literal('CreateExtensionVersionSuccessResult'),
22
- extensionVersion: z.object({
23
- id: z.string(),
24
- }),
25
- signedUrl: z.string(),
26
- }),
27
- z.object({
28
- __typename: z.literal('AuthorizationError'),
29
- message: z.string(),
30
- }),
31
- z.object({
32
- __typename: z.literal('GenericInternalError'),
33
- message: z.string(),
34
- }),
35
- z.object({
36
- __typename: z.literal('GenericUserError'),
37
- message: z.string(),
38
- }),
39
- ]);
40
-
41
- const graphQlErrorSchema = z.object({
42
- message: z.string(),
43
- });
44
-
45
- const createExtensionVersionResultSchema = z.object({
46
- errors: z.array(graphQlErrorSchema).optional().nullable(),
47
- data: z
48
- .object({
49
- createExtensionVersion: createExtensionVersionResultDataSchema,
50
- })
51
- .nullable(),
52
- });
23
+ const ignoreGlobs = [
24
+ '**/.git/**/*',
25
+ '.git/**/*',
26
+ 'eslint.config.js',
27
+ '**/eslint.config.js',
28
+ 'node_modules/**/*',
29
+ '**/node_modules/**/*',
30
+ 'yalc.lock',
31
+ '**/yalc.lock',
32
+ ];
53
33
 
54
34
  type CreateExtensionVersionOptions = {
55
35
  extensionId?: string;
56
36
  versionName: string;
37
+ configFile?: string;
57
38
  };
58
39
 
59
40
  export const createExtensionVersion = async (
@@ -84,6 +65,20 @@ export const createExtensionVersion = async (
84
65
  enableServer = false;
85
66
  enableDb = false;
86
67
  }
68
+ let config: Record<string, string> | null = null;
69
+ if (opts.configFile) {
70
+ const configFilePath = resolve(cwd, opts.configFile);
71
+ if (opts.verbose) {
72
+ console.log(`Loading config file ${configFilePath}`);
73
+ }
74
+ const configFile = await readFile(configFilePath, 'utf8');
75
+ try {
76
+ config = parseEnv(configFile) as Record<string, string>;
77
+ } catch (e) {
78
+ console.error(`Failed to parse config file at ${configFilePath}:`, parseErrorMessage(e));
79
+ process.exit(1);
80
+ }
81
+ }
87
82
 
88
83
  const { versionName } = opts;
89
84
  console.log(`Creating extension version ${versionName} (Extension ID ${extensionId})
@@ -98,69 +93,27 @@ export const createExtensionVersion = async (
98
93
  if (opts.verbose) {
99
94
  console.log(`POST ${url}`);
100
95
  }
101
- const createResult = await fetch(url, {
102
- method: 'POST',
103
- headers: {
104
- 'Content-Type': 'application/json',
105
- Authorization: `Bearer ${apiToken}`,
106
- },
107
- body: JSON.stringify({
108
- query: `
109
- mutation ($input: CreateExtensionVersionInput!) {
110
- createExtensionVersion(input: $input) {
111
- __typename
112
- ... on CreateExtensionVersionSuccessResult {
113
- extensionVersion {
114
- id
115
- }
116
- signedUrl
117
- }
118
- ... on Error {
119
- message
120
- }
121
- }
122
- }
123
- `,
124
- variables: {
125
- input: {
126
- extensionId,
127
- extensionName: extConfig.name,
128
- versionName,
129
- enableServer,
130
- enableDb,
131
- },
96
+ const result = await createExtensionVersionRequest({
97
+ client: new GraphQLClient(url),
98
+ variables: {
99
+ input: {
100
+ extensionId,
101
+ extensionName: extConfig.name,
102
+ versionName,
103
+ enableServer,
104
+ enableDb,
105
+ config: config ? JSON.stringify(config) : undefined,
132
106
  },
133
- }),
107
+ },
108
+ sessionAuthToken: apiToken,
134
109
  });
135
- if (!createResult.ok) {
136
- const message = `Failed to create an extension version (status ${createResult.status})`;
137
- try {
138
- const data = await createResult.json();
139
- console.error(`${message}:`, parseErrorMessage(data));
140
- process.exit(1);
141
- } catch {
142
- console.error(message);
143
- process.exit(1);
144
- }
145
- }
146
- const data = await createResult.json();
147
- const parseResult = createExtensionVersionResultSchema.safeParse(data);
148
- if (!parseResult.success) {
149
- console.error('Failed to create an extension version:', parseResult.error.message);
150
- process.exit(1);
151
- }
152
- const result = parseResult.data;
153
- if (result.errors) {
154
- console.error('Failed to create an extension version:', parseErrorMessage(result));
155
- process.exit(1);
156
- }
157
- if (result.data?.createExtensionVersion.__typename === 'CreateExtensionVersionSuccessResult') {
158
- extensionVersionId = result.data.createExtensionVersion.extensionVersion.id;
159
- signedUrl = result.data.createExtensionVersion.signedUrl;
110
+ if (result.createExtensionVersion.__typename === 'CreateExtensionVersionSuccessResult') {
111
+ extensionVersionId = result.createExtensionVersion.extensionVersion.id;
112
+ signedUrl = result.createExtensionVersion.signedUrl;
160
113
  } else {
161
114
  console.error(
162
- `Failed to create an extension version (${result.data?.createExtensionVersion.__typename ?? 'unknown error'}):`,
163
- parseErrorMessage(result.data?.createExtensionVersion),
115
+ `Failed to create an extension version (${result.createExtensionVersion.__typename}):`,
116
+ result.createExtensionVersion.message,
164
117
  );
165
118
  process.exit(1);
166
119
  }
@@ -197,14 +150,7 @@ export const createExtensionVersion = async (
197
150
 
198
151
  archive.glob('**/*', {
199
152
  cwd,
200
- ignore: [
201
- '**/.git/**/*',
202
- '.git/**/*',
203
- 'eslint.config.js',
204
- '**/eslint.config.js',
205
- 'node_modules/**/*',
206
- '**/node_modules/**/*',
207
- ],
153
+ ignore: ignoreGlobs,
208
154
  });
209
155
 
210
156
  if (opts.verbose) {
@@ -67,8 +67,7 @@ export const createUser = async (
67
67
 
68
68
  let platformId: string | null | undefined = opts.platformId;
69
69
  let orgId: string | null | undefined = opts.orgId;
70
- // TODO
71
- // let orgPrimary = false;
70
+ let orgPrimary = false;
72
71
 
73
72
  try {
74
73
  const user = await db.transaction(async (db) => {
@@ -115,7 +114,7 @@ export const createUser = async (
115
114
  if (opts.verbose) {
116
115
  console.log(`Created org ID ${orgId}`);
117
116
  }
118
- // orgPrimary = true;
117
+ orgPrimary = true;
119
118
  }
120
119
 
121
120
  const user = await db.one(
@@ -124,9 +123,13 @@ export const createUser = async (
124
123
  )`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
124
  );
126
125
 
127
- // if (opts.admin) {
128
- // await grantAllUserPlatformPermissions({ userId: user.id, platformId }, db);
129
- // }
126
+ if (orgPrimary) {
127
+ await db.maybeOne(
128
+ sql.type(
129
+ voidSelectSchema,
130
+ )`update "Orgs" set "primaryUserId" = ${user.id} where "id" = ${orgId}`,
131
+ );
132
+ }
130
133
 
131
134
  return user;
132
135
  });
@@ -1,19 +1,31 @@
1
+ import { cleanSmallId } from '@wirechunk/lib/clean-small-id.ts';
1
2
  import { createPool, sql, UniqueIntegrityConstraintViolationError } from 'slonik';
2
3
  import { z } from 'zod';
3
4
  import type { Env } from '../env.ts';
4
5
  import { requireCoreDbUrl } from '../env.ts';
5
6
  import { detailedUniqueIntegrityConstraintViolationError } from '../errors.ts';
6
7
  import type { WithGlobalOptions } from '../global-options.ts';
8
+ import {
9
+ grantAllUserPlatformPermissions,
10
+ revokeAllUserPlatformPermissions,
11
+ } from '../users/permissions.ts';
12
+ import { voidSelectSchema } from '../util.ts';
7
13
 
8
- const findUserSchema = z.object({
14
+ const findPlatformAdminSchema = z.object({
9
15
  id: z.string(),
10
16
  platformId: z.string(),
17
+ active: z.boolean(),
18
+ });
19
+
20
+ const findUserSchema = z.object({
21
+ id: z.string(),
11
22
  });
12
23
 
13
24
  type EditAdminOptions = {
14
25
  platformId: string;
15
26
  userId: string;
16
27
  owner?: boolean;
28
+ active?: boolean;
17
29
  revokeAllPermissions?: boolean;
18
30
  };
19
31
 
@@ -22,7 +34,7 @@ export const editAdmin = async (
22
34
  env: Env,
23
35
  ): Promise<void> => {
24
36
  const db = await createPool(requireCoreDbUrl(env));
25
- const { platformId, userId, owner, revokeAllPermissions } = opts;
37
+ const { platformId, userId, owner, active, revokeAllPermissions } = opts;
26
38
 
27
39
  if (owner && revokeAllPermissions) {
28
40
  console.error(
@@ -33,9 +45,9 @@ export const editAdmin = async (
33
45
 
34
46
  try {
35
47
  await db.transaction(async (db) => {
36
- const platformAdmin = await db.maybeOne(
48
+ let platformAdmin = await db.maybeOne(
37
49
  sql.type(
38
- findUserSchema,
50
+ findPlatformAdminSchema,
39
51
  )`select "id" from "PlatformAdmins" where "platformId" = ${platformId} and "userId" = ${userId}`,
40
52
  );
41
53
  if (!platformAdmin) {
@@ -46,17 +58,67 @@ export const editAdmin = async (
46
58
  throw new Error(`User with ID ${userId} not found`);
47
59
  }
48
60
  }
49
-
50
- // TODO
51
-
52
- // if (owner) {
53
- // await grantAllUserPlatformPermissions({ userId: id, platformId: user.platformId }, db);
54
- // console.log('Set user as a platform admin');
55
- // }
56
- // if (revokeAllPermissions) {
57
- // await revokeAllUserPlatformPermissions({ userId: id }, db);
58
- // console.log('Revoked all platform permissions of user');
59
- // }
61
+ if (owner) {
62
+ if (!platformAdmin) {
63
+ platformAdmin = await db.one(
64
+ sql.type(findPlatformAdminSchema)`
65
+ insert into "PlatformAdmins" ("id", "platformId", "userId", "owner", "active")
66
+ values (${cleanSmallId()}, ${platformId}, ${userId}, ${active ?? true}, true)
67
+ returning "id", "platformId", "active"
68
+ `,
69
+ );
70
+ }
71
+ await grantAllUserPlatformPermissions({ platformAdminId: platformAdmin.id }, db);
72
+ if (opts.verbose) {
73
+ console.log('Set the user as an owner on the platform');
74
+ }
75
+ } else if (owner === false) {
76
+ if (platformAdmin) {
77
+ await db.query(
78
+ sql.type(voidSelectSchema)`
79
+ update "PlatformAdmins"
80
+ set "owner" = false
81
+ where "id" = ${platformAdmin.id}
82
+ `,
83
+ );
84
+ if (opts.verbose) {
85
+ console.log('Removed the user’s owner privileges on the platform');
86
+ }
87
+ } else {
88
+ console.log('This user is not an admin on this platform');
89
+ }
90
+ }
91
+ if (typeof active === 'boolean') {
92
+ if (platformAdmin) {
93
+ await db.query(
94
+ sql.type(voidSelectSchema)`
95
+ update "PlatformAdmins"
96
+ set "active" = ${active}
97
+ where "id" = ${platformAdmin.id}
98
+ `,
99
+ );
100
+ } else {
101
+ if (active) {
102
+ // Automatically create a platform admin.
103
+ await db.one(
104
+ sql.type(voidSelectSchema)`
105
+ insert into "PlatformAdmins" ("id", "platformId", "userId", "owner", "active")
106
+ values (${cleanSmallId()}, ${platformId}, ${userId}, false, ${active})
107
+ `,
108
+ );
109
+ } else {
110
+ console.log('This user is not an admin on this platform');
111
+ }
112
+ }
113
+ }
114
+ if (revokeAllPermissions) {
115
+ if (platformAdmin) {
116
+ await revokeAllUserPlatformPermissions({ platformAdminId: platformAdmin.id }, db);
117
+ console.log('Revoked all platform permissions of user');
118
+ } else {
119
+ console.log('This user is not an admin on this platform');
120
+ }
121
+ }
60
122
  });
61
123
  } catch (e) {
62
124
  if (e instanceof UniqueIntegrityConstraintViolationError) {
@@ -5,13 +5,13 @@ import type { Env } from '../../env.ts';
5
5
  import type { WithGlobalOptions } from '../../global-options.js';
6
6
  import { getExtensionDbConnectInfo, replaceEnvVar } from '../../util.ts';
7
7
 
8
- type DbConnectInfoOptions = {
8
+ type GetDbUrlOptions = {
9
9
  extensionId?: string;
10
10
  dbName?: string;
11
11
  };
12
12
 
13
- export const dbConnectInfo = async (
14
- opts: WithGlobalOptions<DbConnectInfoOptions>,
13
+ export const getDbUrl = async (
14
+ opts: WithGlobalOptions<GetDbUrlOptions>,
15
15
  env: Env,
16
16
  ): Promise<void> => {
17
17
  const connInfo = getExtensionDbConnectInfo(opts, env);
@@ -10,7 +10,7 @@ import {
10
10
  getExtensionDbConnectInfo,
11
11
  requireExtensionIdOptionOrEnvVar,
12
12
  } from '../../util.ts';
13
- import { dbConnectInfo } from './db-connect-info.ts';
13
+ import { getDbUrl } from './get-db-url.ts';
14
14
 
15
15
  const initExtDb = async ({
16
16
  // A connection to the extension database by a superuser role.
@@ -149,7 +149,8 @@ export const initDb = async (
149
149
  const coreDbUrl = requireCoreDbUrl(env);
150
150
  const extensionId = requireExtensionIdOptionOrEnvVar(opts, env);
151
151
 
152
- await dbConnectInfo(opts, env);
152
+ // This writes the DATABASE_URL environment variable to a .env.local (or .env.<env-mode>.local) file.
153
+ await getDbUrl(opts, env);
153
154
 
154
155
  const db = await createPool(coreDbUrl, dbPoolOptions(opts));
155
156