@cedarjs/cli 8.0.0-canary.3525 → 8.0.0-canary.3526
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/dist/commands/setup/tenancy/authCodemod.js +19 -5
- package/dist/commands/setup/tenancy/dbCodemod.js +16 -7
- package/dist/commands/setup/tenancy/graphqlCodemod.js +10 -1
- package/dist/commands/setup/tenancy/schemaPrisma.js +67 -5
- package/dist/commands/setup/tenancy/signupCodemod.js +16 -7
- package/dist/commands/setup/tenancy/templates/ensureDefaultOrganizations.dataMigration.ts.template +2 -1
- package/dist/commands/setup/tenancy/templates/organizations.scenarios.ts.template +3 -2
- package/dist/commands/setup/tenancy/tenancyHandler.js +80 -32
- package/package.json +13 -13
|
@@ -53,12 +53,26 @@ function transform(fileInfo) {
|
|
|
53
53
|
if (!selectFound) {
|
|
54
54
|
throw new Error("CEDAR_CODEMOD_ERR_AUTH_SHAPE_NOT_FOUND");
|
|
55
55
|
}
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
const exported = /* @__PURE__ */ new Set();
|
|
57
|
+
root.find(j.ExportNamedDeclaration, { source: { value: "@cedarjs/tenancy" } }).forEach((path) => {
|
|
58
|
+
for (const specifier of path.node.specifiers ?? []) {
|
|
59
|
+
if (j.Identifier.check(specifier.exported)) {
|
|
60
|
+
exported.add(specifier.exported.name);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
61
63
|
});
|
|
64
|
+
const missing = ["hasOrgRole", "requireMembership"].filter(
|
|
65
|
+
(name) => !exported.has(name)
|
|
66
|
+
);
|
|
67
|
+
if (missing.length > 0) {
|
|
68
|
+
const tenancyExport = j.withParser("ts")(
|
|
69
|
+
`export { ${missing.join(", ")} } from '@cedarjs/tenancy'
|
|
70
|
+
`
|
|
71
|
+
).find(j.ExportNamedDeclaration).nodes()[0];
|
|
72
|
+
root.find(j.Program).forEach((path) => {
|
|
73
|
+
path.node.body.push(tenancyExport);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
62
76
|
return root.toSource();
|
|
63
77
|
}
|
|
64
78
|
export {
|
|
@@ -2,14 +2,23 @@ import j from "jscodeshift";
|
|
|
2
2
|
const DEFAULT_GLOBAL_MODELS = ["user", "organization", "membership"];
|
|
3
3
|
function transform(fileInfo, _api, options) {
|
|
4
4
|
const root = j.withParser("tsx")(fileInfo.source);
|
|
5
|
+
if (root.find(j.CallExpression, {
|
|
6
|
+
callee: { name: "createTenancyExtension" }
|
|
7
|
+
}).length > 0) {
|
|
8
|
+
return fileInfo.source;
|
|
9
|
+
}
|
|
10
|
+
const hasImport = root.find(j.ImportSpecifier, {
|
|
11
|
+
imported: { name: "createTenancyExtension" }
|
|
12
|
+
}).length > 0;
|
|
13
|
+
if (!hasImport) {
|
|
14
|
+
root.find(j.ImportDeclaration).at(-1).insertAfter(
|
|
15
|
+
j.importDeclaration(
|
|
16
|
+
[j.importSpecifier(j.identifier("createTenancyExtension"))],
|
|
17
|
+
j.literal("@cedarjs/tenancy")
|
|
18
|
+
)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
5
21
|
const tenantField = typeof options.tenantField === "string" ? options.tenantField : void 0;
|
|
6
|
-
const imports = root.find(j.ImportDeclaration);
|
|
7
|
-
imports.at(-1).insertAfter(
|
|
8
|
-
j.importDeclaration(
|
|
9
|
-
[j.importSpecifier(j.identifier("createTenancyExtension"))],
|
|
10
|
-
j.literal("@cedarjs/tenancy")
|
|
11
|
-
)
|
|
12
|
-
);
|
|
13
22
|
const configProperties = [
|
|
14
23
|
...tenantField ? [
|
|
15
24
|
j.objectProperty(
|
|
@@ -47,16 +47,25 @@ function buildContextProperty() {
|
|
|
47
47
|
return property;
|
|
48
48
|
}
|
|
49
49
|
function addNamedImport(root, names, source) {
|
|
50
|
+
const missing = names.filter(
|
|
51
|
+
(name) => root.find(j.ImportSpecifier, { local: { name } }).length === 0
|
|
52
|
+
);
|
|
53
|
+
if (missing.length === 0) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
50
56
|
const imports = root.find(j.ImportDeclaration);
|
|
51
57
|
imports.at(-1).insertAfter(
|
|
52
58
|
j.importDeclaration(
|
|
53
|
-
|
|
59
|
+
missing.map((name) => j.importSpecifier(j.identifier(name))),
|
|
54
60
|
j.literal(source)
|
|
55
61
|
)
|
|
56
62
|
);
|
|
57
63
|
}
|
|
58
64
|
function transform(fileInfo) {
|
|
59
65
|
const root = j.withParser("tsx")(fileInfo.source);
|
|
66
|
+
if (root.find(j.CallExpression, { callee: { name: "resolveCurrentOrg" } }).length > 0) {
|
|
67
|
+
return fileInfo.source;
|
|
68
|
+
}
|
|
60
69
|
const handlerCalls = root.find(j.CallExpression, {
|
|
61
70
|
callee: { name: "createGraphQLHandler" }
|
|
62
71
|
});
|
|
@@ -15,7 +15,7 @@ const MEMBERSHIP_MODEL = `model Membership {
|
|
|
15
15
|
invitedById String?
|
|
16
16
|
invitationToken String? @unique
|
|
17
17
|
createdAt DateTime @default(now())
|
|
18
|
-
updatedAt
|
|
18
|
+
updatedAt DateTime @updatedAt
|
|
19
19
|
user User? @relation(fields: [userId], references: [id])
|
|
20
20
|
organization Organization @relation(fields: [organizationId], references: [id])
|
|
21
21
|
invitedBy Membership? @relation("Inviter", fields: [invitedById], references: [id])
|
|
@@ -30,6 +30,10 @@ const RW_DATA_MIGRATION_MODEL = `model RW_DataMigration {
|
|
|
30
30
|
startedAt DateTime
|
|
31
31
|
finishedAt DateTime
|
|
32
32
|
}`;
|
|
33
|
+
const collapseWhitespace = (text) => text.replace(/\s+/g, " ").trim();
|
|
34
|
+
function hasCanonicalModel(schema, model) {
|
|
35
|
+
return collapseWhitespace(schema).includes(collapseWhitespace(model));
|
|
36
|
+
}
|
|
33
37
|
function hasModel(schema, modelName) {
|
|
34
38
|
return new RegExp(`\\bmodel\\s+${modelName}\\s*\\{`).test(schema);
|
|
35
39
|
}
|
|
@@ -37,7 +41,7 @@ function addTenancyModels(schema) {
|
|
|
37
41
|
return [ORGANIZATION_MODEL, MEMBERSHIP_MODEL].reduce(appendModel, schema);
|
|
38
42
|
}
|
|
39
43
|
function appendModel(schema, model) {
|
|
40
|
-
if (schema
|
|
44
|
+
if (hasCanonicalModel(schema, model)) {
|
|
41
45
|
return schema;
|
|
42
46
|
}
|
|
43
47
|
return `${schema.trimEnd()}
|
|
@@ -58,8 +62,8 @@ function editSchema(schema, options) {
|
|
|
58
62
|
if (!hasModel(schema, "User")) {
|
|
59
63
|
throw new Error("CEDAR_TENANCY_ERR_NO_USER_MODEL");
|
|
60
64
|
}
|
|
61
|
-
const organizationIsCustomized = hasModel(schema, "Organization") && !schema
|
|
62
|
-
const membershipIsCustomized = hasModel(schema, "Membership") && !schema
|
|
65
|
+
const organizationIsCustomized = hasModel(schema, "Organization") && !hasCanonicalModel(schema, ORGANIZATION_MODEL);
|
|
66
|
+
const membershipIsCustomized = hasModel(schema, "Membership") && !hasCanonicalModel(schema, MEMBERSHIP_MODEL);
|
|
63
67
|
const modelsCustomized = organizationIsCustomized || membershipIsCustomized;
|
|
64
68
|
if (modelsCustomized && !options.force) {
|
|
65
69
|
return { schema: addDataMigrationModel(schema), outcome: "skipped" };
|
|
@@ -70,12 +74,70 @@ function editSchema(schema, options) {
|
|
|
70
74
|
outcome: modelsCustomized ? "forced" : "added"
|
|
71
75
|
};
|
|
72
76
|
}
|
|
77
|
+
const SCALAR_PLACEHOLDERS = {
|
|
78
|
+
String: "'__NAME__ __N__'",
|
|
79
|
+
Int: "__N__",
|
|
80
|
+
BigInt: "__N__",
|
|
81
|
+
Float: "__N__",
|
|
82
|
+
Decimal: "__N__",
|
|
83
|
+
Boolean: "false",
|
|
84
|
+
DateTime: "new Date()",
|
|
85
|
+
Json: "{}",
|
|
86
|
+
Bytes: "new Uint8Array()"
|
|
87
|
+
};
|
|
88
|
+
const FILLED_USER_FIELDS = ["email", "hashedPassword", "salt"];
|
|
89
|
+
function requiredUserFields(schema) {
|
|
90
|
+
const body = schema.match(/\bmodel\s+User\s*\{([\s\S]*?)\n\}/)?.[1];
|
|
91
|
+
if (!body) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const enumValues = (name) => schema.match(new RegExp(`\\benum\\s+${name}\\s*\\{([\\s\\S]*?)\\}`))?.[1].split("\n").map(
|
|
95
|
+
(line) => line.replace(/\/\/.*/, "").trim().match(/^([A-Za-z_]\w*)(?:\s|$)/)?.[1]
|
|
96
|
+
).find((member) => member !== void 0);
|
|
97
|
+
const relationKeys = [
|
|
98
|
+
...body.matchAll(/@relation\([^)]*fields:\s*\[([^\]]*)\]/g)
|
|
99
|
+
].flatMap((match) => match[1].split(",").map((key) => key.trim()));
|
|
100
|
+
return body.split("\n").flatMap((line) => {
|
|
101
|
+
const field = line.replace(/\/\/.*/, "").trim().match(/^(\w+)\s+(\w+)(\?|\[\])?(.*)$/);
|
|
102
|
+
if (!field) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const [, name, type, modifier, attributes] = field;
|
|
106
|
+
if (modifier || FILLED_USER_FIELDS.includes(name) || relationKeys.includes(name) || /@(default|updatedAt|relation)\b/.test(attributes)) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
const scalar = SCALAR_PLACEHOLDERS[type];
|
|
110
|
+
if (scalar) {
|
|
111
|
+
return [{ name, value: scalar.replace("__NAME__", name) }];
|
|
112
|
+
}
|
|
113
|
+
const enumValue = enumValues(type);
|
|
114
|
+
return enumValue ? [{ name, value: `'${enumValue}'` }] : [];
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function addRequiredUserFields(source, fields, { firstNumber = 1 } = {}) {
|
|
118
|
+
if (fields.length === 0) {
|
|
119
|
+
return source;
|
|
120
|
+
}
|
|
121
|
+
let n = firstNumber - 1;
|
|
122
|
+
return source.replace(/^(\s*)salt: 'x',$/gm, (line, indent) => {
|
|
123
|
+
n++;
|
|
124
|
+
return [
|
|
125
|
+
line,
|
|
126
|
+
...fields.map(
|
|
127
|
+
(field) => `${indent}${field.name}: ${field.value.replaceAll("__N__", String(n))},`
|
|
128
|
+
)
|
|
129
|
+
].join("\n");
|
|
130
|
+
});
|
|
131
|
+
}
|
|
73
132
|
export {
|
|
74
133
|
MEMBERSHIP_MODEL,
|
|
75
134
|
ORGANIZATION_MODEL,
|
|
76
135
|
RW_DATA_MIGRATION_MODEL,
|
|
77
136
|
addDataMigrationModel,
|
|
137
|
+
addRequiredUserFields,
|
|
78
138
|
addTenancyModels,
|
|
79
139
|
editSchema,
|
|
80
|
-
|
|
140
|
+
hasCanonicalModel,
|
|
141
|
+
hasModel,
|
|
142
|
+
requiredUserFields
|
|
81
143
|
};
|
|
@@ -110,6 +110,14 @@ function wrapCreateCall(fn) {
|
|
|
110
110
|
}
|
|
111
111
|
function transform(fileInfo) {
|
|
112
112
|
const root = j.withParser("tsx")(fileInfo.source);
|
|
113
|
+
if (root.find(j.CallExpression, {
|
|
114
|
+
callee: { name: "ensureDefaultOrganization" }
|
|
115
|
+
}).length > 0) {
|
|
116
|
+
return fileInfo.source;
|
|
117
|
+
}
|
|
118
|
+
const hasImport = root.find(j.ImportSpecifier, {
|
|
119
|
+
local: { name: "ensureDefaultOrganization" }
|
|
120
|
+
}).length > 0;
|
|
113
121
|
const signupOptionsDeclarators = root.find(j.VariableDeclarator, {
|
|
114
122
|
id: { name: "signupOptions" }
|
|
115
123
|
});
|
|
@@ -140,13 +148,14 @@ function transform(fileInfo) {
|
|
|
140
148
|
throw new Error("CEDAR_CODEMOD_ERR_SIGNUP_SHAPE_NOT_FOUND");
|
|
141
149
|
}
|
|
142
150
|
addInvitationTokenToUserAttributes(root);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
151
|
+
if (!hasImport) {
|
|
152
|
+
root.find(j.ImportDeclaration).at(-1).insertAfter(
|
|
153
|
+
j.importDeclaration(
|
|
154
|
+
[j.importSpecifier(j.identifier("ensureDefaultOrganization"))],
|
|
155
|
+
j.literal("src/services/organizations/organizations")
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
150
159
|
return root.toSource();
|
|
151
160
|
}
|
|
152
161
|
export {
|
package/dist/commands/setup/tenancy/templates/ensureDefaultOrganizations.dataMigration.ts.template
CHANGED
|
@@ -11,7 +11,8 @@ const BATCH_SIZE = 100
|
|
|
11
11
|
*
|
|
12
12
|
* Run with `__DATA_MIGRATE_UP_COMMAND__`, after
|
|
13
13
|
* `__PRISMA_MIGRATE_DEV_COMMAND__` has applied the
|
|
14
|
-
* `Organization`/`Membership` migration
|
|
14
|
+
* `Organization`/`Membership` migration and `__PRISMA_GENERATE_COMMAND__`
|
|
15
|
+
* has updated the Prisma client.
|
|
15
16
|
*/
|
|
16
17
|
export default async ({ db }: { db: PrismaClient }) => {
|
|
17
18
|
let cursor: string | undefined
|
|
@@ -3,8 +3,9 @@ import type { ScenarioData } from '@cedarjs/testing/api'
|
|
|
3
3
|
import type { Membership, Organization, Prisma } from 'src/lib/db'
|
|
4
4
|
|
|
5
5
|
// This scenario assumes a dbAuth-style `User` model (`email`,
|
|
6
|
-
// `hashedPassword`, `salt`)
|
|
7
|
-
//
|
|
6
|
+
// `hashedPassword`, `salt`), plus placeholders for any other fields your
|
|
7
|
+
// `User` requires. Adjust the `user` fixtures below if you use a different
|
|
8
|
+
// auth provider.
|
|
8
9
|
export const standard = defineScenario<
|
|
9
10
|
Prisma.OrganizationCreateArgs | Prisma.MembershipCreateArgs
|
|
10
11
|
>({
|
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
addWebPackages
|
|
12
12
|
} from "@cedarjs/cli-helpers/installHelpers";
|
|
13
13
|
import { formatCedarCommand } from "@cedarjs/cli-helpers/packageManager/display";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
runBinSync,
|
|
16
|
+
runTransitiveBin
|
|
17
|
+
} from "@cedarjs/cli-helpers/packageManager/exec";
|
|
15
18
|
import { getDataMigrationsPath, getSchemaPath } from "@cedarjs/project-config";
|
|
16
19
|
import { errorTelemetry } from "@cedarjs/telemetry";
|
|
17
20
|
import { getPaths, transformTSToJS, writeFile } from "../../../lib/index.js";
|
|
@@ -23,10 +26,12 @@ import {
|
|
|
23
26
|
noUserModelMessage
|
|
24
27
|
} from "./preflight.js";
|
|
25
28
|
import {
|
|
29
|
+
addRequiredUserFields,
|
|
26
30
|
editSchema,
|
|
27
31
|
hasModel,
|
|
28
32
|
MEMBERSHIP_MODEL,
|
|
29
|
-
ORGANIZATION_MODEL
|
|
33
|
+
ORGANIZATION_MODEL,
|
|
34
|
+
requiredUserFields
|
|
30
35
|
} from "./schemaPrisma.js";
|
|
31
36
|
const DEFAULT_TENANT_FIELD = "organizationId";
|
|
32
37
|
const AUTH_SNIPPET = `
|
|
@@ -87,14 +92,22 @@ const handler = async ({ tenantField, force }) => {
|
|
|
87
92
|
const graphqlPath = path.join(paths.api.functions, `graphql.${ext}`);
|
|
88
93
|
const functionsAuthPath = path.join(paths.api.functions, `auth.${ext}`);
|
|
89
94
|
const isDbAuthProject = fs.existsSync(functionsAuthPath) && fs.readFileSync(functionsAuthPath, "utf-8").includes("DbAuthHandler");
|
|
90
|
-
const
|
|
95
|
+
const writtenFiles = [];
|
|
96
|
+
const keptFiles = [];
|
|
97
|
+
const writeTemplateFile = async (templateFile, outputPath, replacements = {}, transform = (content) => content) => {
|
|
91
98
|
const dirname = import.meta.dirname;
|
|
92
99
|
const templatePath = path.resolve(dirname, "templates", templateFile);
|
|
93
100
|
let templateContent = fs.readFileSync(templatePath, "utf-8");
|
|
94
101
|
for (const [token, value] of Object.entries(replacements)) {
|
|
95
102
|
templateContent = templateContent.replaceAll(token, value);
|
|
96
103
|
}
|
|
104
|
+
templateContent = transform(templateContent);
|
|
97
105
|
const content = projectIsTypescript ? templateContent : await transformTSToJS(outputPath, templateContent);
|
|
106
|
+
if (fs.existsSync(outputPath) && !force) {
|
|
107
|
+
keptFiles.push(outputPath);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
writtenFiles.push(outputPath);
|
|
98
111
|
return writeFile(outputPath, content, { overwriteExisting: force });
|
|
99
112
|
};
|
|
100
113
|
const tasks = new Listr(
|
|
@@ -153,9 +166,9 @@ Re-run with --force to have this command append its own versions beside them ins
|
|
|
153
166
|
"`prisma format` failed, so the `memberships` relation was not added to the `User` model. Add it by hand: `memberships Membership[]` as the last field of `model User { ... }`, then re-run this command."
|
|
154
167
|
);
|
|
155
168
|
}
|
|
156
|
-
const membershipsOccurrences = (schema2) => (schema2.match(/memberships\s+Membership\[\]/g) ?? []).length;
|
|
157
169
|
const formattedSchema = fs.readFileSync(schemaPath, "utf-8");
|
|
158
|
-
|
|
170
|
+
const userModel = formattedSchema.match(/\bmodel\s+User\s*\{[\s\S]*?\n\}/)?.[0] ?? "";
|
|
171
|
+
if (!/^\s*memberships\s+Membership\[\]/m.test(userModel)) {
|
|
159
172
|
if (editResult.outcome === "forced") {
|
|
160
173
|
task.output = 'The `memberships` relation was not added to the `User` model, likely because of the clash from --force. Continuing with the rest of the setup; see "Next steps" for how to merge the appended models by hand.';
|
|
161
174
|
return;
|
|
@@ -260,17 +273,28 @@ ${SIGNUP_SNIPPET}`;
|
|
|
260
273
|
title: "Adding the organizations service...",
|
|
261
274
|
task: async () => {
|
|
262
275
|
const serviceDir = path.join(paths.api.services, "organizations");
|
|
276
|
+
const userFields = requiredUserFields(
|
|
277
|
+
fs.readFileSync(
|
|
278
|
+
await getSchemaPath(paths.api.prismaConfig),
|
|
279
|
+
"utf-8"
|
|
280
|
+
)
|
|
281
|
+
);
|
|
282
|
+
const withUserFields = (firstNumber) => (content) => addRequiredUserFields(content, userFields, { firstNumber });
|
|
263
283
|
await writeTemplateFile(
|
|
264
284
|
"organizations.ts.template",
|
|
265
285
|
path.join(serviceDir, `organizations.${ext}`)
|
|
266
286
|
);
|
|
267
287
|
await writeTemplateFile(
|
|
268
288
|
"organizations.test.ts.template",
|
|
269
|
-
path.join(serviceDir, `organizations.test.${ext}`)
|
|
289
|
+
path.join(serviceDir, `organizations.test.${ext}`),
|
|
290
|
+
{},
|
|
291
|
+
withUserFields(101)
|
|
270
292
|
);
|
|
271
293
|
await writeTemplateFile(
|
|
272
294
|
"organizations.scenarios.ts.template",
|
|
273
|
-
path.join(serviceDir, `organizations.scenarios.${ext}`)
|
|
295
|
+
path.join(serviceDir, `organizations.scenarios.${ext}`),
|
|
296
|
+
{},
|
|
297
|
+
withUserFields(1)
|
|
274
298
|
);
|
|
275
299
|
await writeTemplateFile(
|
|
276
300
|
"organizations.sdl.ts.template",
|
|
@@ -280,10 +304,17 @@ ${SIGNUP_SNIPPET}`;
|
|
|
280
304
|
},
|
|
281
305
|
{
|
|
282
306
|
title: "Adding a data migration to backfill existing users...",
|
|
283
|
-
task: async () => {
|
|
307
|
+
task: async (_ctx, task) => {
|
|
284
308
|
const dataMigrationsPath = await getDataMigrationsPath(
|
|
285
309
|
paths.api.prismaConfig
|
|
286
310
|
);
|
|
311
|
+
const existing = fs.existsSync(dataMigrationsPath) && fs.readdirSync(dataMigrationsPath).find(
|
|
312
|
+
(file) => /-ensure-default-organizations\.[jt]s$/.test(file)
|
|
313
|
+
);
|
|
314
|
+
if (existing) {
|
|
315
|
+
task.skip(`Already added: ${existing}`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
287
318
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split(".")[0].replace(/\D/g, "");
|
|
288
319
|
await writeTemplateFile(
|
|
289
320
|
"ensureDefaultOrganizations.dataMigration.ts.template",
|
|
@@ -300,6 +331,10 @@ ${SIGNUP_SNIPPET}`;
|
|
|
300
331
|
"prisma",
|
|
301
332
|
"migrate",
|
|
302
333
|
"dev"
|
|
334
|
+
]),
|
|
335
|
+
__PRISMA_GENERATE_COMMAND__: formatCedarCommand([
|
|
336
|
+
"prisma",
|
|
337
|
+
"generate"
|
|
303
338
|
])
|
|
304
339
|
}
|
|
305
340
|
);
|
|
@@ -327,34 +362,39 @@ ${SIGNUP_SNIPPET}`;
|
|
|
327
362
|
}
|
|
328
363
|
},
|
|
329
364
|
{
|
|
330
|
-
title: "
|
|
365
|
+
title: "Formatting and linting changed files...",
|
|
331
366
|
task: async (_ctx, task) => {
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
path.join(paths.api.lib,
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
path.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
for (const
|
|
367
|
+
const changedFiles = [
|
|
368
|
+
...writtenFiles,
|
|
369
|
+
...["db", "auth"].map((name) => path.join(paths.api.lib, name)),
|
|
370
|
+
...["graphql", "auth"].map(
|
|
371
|
+
(name) => path.join(paths.api.functions, name)
|
|
372
|
+
)
|
|
373
|
+
].flatMap(
|
|
374
|
+
(file) => path.extname(file) ? [file] : [`${file}.js`, `${file}.ts`]
|
|
375
|
+
).filter((file) => fs.existsSync(file));
|
|
376
|
+
const prettierOptions = await getPrettierOptions();
|
|
377
|
+
for (const file of changedFiles) {
|
|
343
378
|
try {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
const source = fs.readFileSync(prettifyPath, "utf-8");
|
|
348
|
-
const prettierOptions = await getPrettierOptions();
|
|
349
|
-
const prettifiedApp = await format(source, {
|
|
379
|
+
const source = fs.readFileSync(file, "utf-8");
|
|
380
|
+
const formatted = await format(source, {
|
|
350
381
|
...prettierOptions,
|
|
351
382
|
parser: "babel-ts"
|
|
352
383
|
});
|
|
353
|
-
fs.writeFileSync(
|
|
384
|
+
fs.writeFileSync(file, formatted, "utf-8");
|
|
354
385
|
} catch {
|
|
355
386
|
task.output = "Couldn't prettify the changes. Please reformat the files manually if needed.";
|
|
356
387
|
}
|
|
357
388
|
}
|
|
389
|
+
try {
|
|
390
|
+
runBinSync("eslint", ["--fix", ...changedFiles], {
|
|
391
|
+
cwd: paths.base
|
|
392
|
+
});
|
|
393
|
+
} catch {
|
|
394
|
+
task.output = `Some of the changed files still have lint errors. Run ${c.highlight(
|
|
395
|
+
formatCedarCommand(["lint"])
|
|
396
|
+
)} to see them.`;
|
|
397
|
+
}
|
|
358
398
|
}
|
|
359
399
|
},
|
|
360
400
|
{
|
|
@@ -365,6 +405,7 @@ ${SIGNUP_SNIPPET}`;
|
|
|
365
405
|
"migrate",
|
|
366
406
|
"dev"
|
|
367
407
|
]);
|
|
408
|
+
const generateCommand = formatCedarCommand(["prisma", "generate"]);
|
|
368
409
|
const dataMigrateCommand = formatCedarCommand(["data-migrate", "up"]);
|
|
369
410
|
const modelsExistWarning = ctx.modelsExistOutcome === "forced" ? c.error(
|
|
370
411
|
"\nschema.prisma is currently invalid (as requested by --force): merge the appended Organization/Membership blocks with your existing ones, then run `prisma format`.\n"
|
|
@@ -376,12 +417,14 @@ ${SIGNUP_SNIPPET}`;
|
|
|
376
417
|
${c.success("\nMulti-tenancy configured!\n")}
|
|
377
418
|
${modelsExistWarning}
|
|
378
419
|
1. Run ${c.highlight(migrateCommand)} to create the Organization/Membership
|
|
379
|
-
tables
|
|
380
|
-
user a
|
|
420
|
+
tables and ${c.highlight(generateCommand)} to update the Prisma client,
|
|
421
|
+
then ${c.highlight(dataMigrateCommand)} to give every existing user a
|
|
422
|
+
default organization.
|
|
381
423
|
2. \`db.ts\`'s tenancy extension scopes every model except
|
|
382
424
|
User/Organization/Membership. For each existing model (e.g.
|
|
383
|
-
Contact) whose rows belong to one organization, add
|
|
384
|
-
|
|
425
|
+
Contact) whose rows belong to one organization, add
|
|
426
|
+
${c.highlight("organizationId String")} plus ${c.highlight("@@index([organizationId])")} to it in
|
|
427
|
+
schema.prisma; for each one that stays global, add it
|
|
385
428
|
to the ${c.highlight("allExcept")} list in api/src/lib/db instead.
|
|
386
429
|
3. Wrap your organization routes with the generated OrgScope in
|
|
387
430
|
web/src/Routes.tsx:
|
|
@@ -399,7 +442,12 @@ ${SIGNUP_SNIPPET}`;
|
|
|
399
442
|
"@cedarjs/tenancy"
|
|
400
443
|
)}.
|
|
401
444
|
|
|
402
|
-
|
|
445
|
+
${keptFiles.length > 0 ? `
|
|
446
|
+
Kept these existing files rather than overwriting them (run with
|
|
447
|
+
${c.highlight("--force")} to overwrite):
|
|
448
|
+
${keptFiles.map((file) => ` ${path.relative(paths.base, file)}`).join("\n")}
|
|
449
|
+
|
|
450
|
+
` : ""} Check out the docs for more info:
|
|
403
451
|
${terminalLink("", "https://cedarjs.com/docs/tenancy")}
|
|
404
452
|
|
|
405
453
|
`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarjs/cli",
|
|
3
|
-
"version": "8.0.0-canary.
|
|
3
|
+
"version": "8.0.0-canary.3526",
|
|
4
4
|
"description": "The CedarJS Command Line",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,17 +36,17 @@
|
|
|
36
36
|
"@babel/preset-typescript": "7.29.7",
|
|
37
37
|
"@babel/traverse": "7.29.10",
|
|
38
38
|
"@babel/types": "7.29.8",
|
|
39
|
-
"@cedarjs/api-server": "8.0.0-canary.
|
|
40
|
-
"@cedarjs/babel-config": "8.0.0-canary.
|
|
41
|
-
"@cedarjs/cli-helpers": "8.0.0-canary.
|
|
42
|
-
"@cedarjs/internal": "8.0.0-canary.
|
|
43
|
-
"@cedarjs/prerender": "8.0.0-canary.
|
|
44
|
-
"@cedarjs/project-config": "8.0.0-canary.
|
|
45
|
-
"@cedarjs/structure": "8.0.0-canary.
|
|
46
|
-
"@cedarjs/telemetry": "8.0.0-canary.
|
|
47
|
-
"@cedarjs/utils": "8.0.0-canary.
|
|
48
|
-
"@cedarjs/vite": "8.0.0-canary.
|
|
49
|
-
"@cedarjs/web-server": "8.0.0-canary.
|
|
39
|
+
"@cedarjs/api-server": "8.0.0-canary.3526",
|
|
40
|
+
"@cedarjs/babel-config": "8.0.0-canary.3526",
|
|
41
|
+
"@cedarjs/cli-helpers": "8.0.0-canary.3526",
|
|
42
|
+
"@cedarjs/internal": "8.0.0-canary.3526",
|
|
43
|
+
"@cedarjs/prerender": "8.0.0-canary.3526",
|
|
44
|
+
"@cedarjs/project-config": "8.0.0-canary.3526",
|
|
45
|
+
"@cedarjs/structure": "8.0.0-canary.3526",
|
|
46
|
+
"@cedarjs/telemetry": "8.0.0-canary.3526",
|
|
47
|
+
"@cedarjs/utils": "8.0.0-canary.3526",
|
|
48
|
+
"@cedarjs/vite": "8.0.0-canary.3526",
|
|
49
|
+
"@cedarjs/web-server": "8.0.0-canary.3526",
|
|
50
50
|
"@listr2/prompt-adapter-enquirer": "4.4.3",
|
|
51
51
|
"@opentelemetry/api": "1.9.1",
|
|
52
52
|
"@opentelemetry/core": "2.12.0",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"yargs": "18.2.0"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
|
97
|
-
"@cedarjs/framework-tools": "8.0.0-canary.
|
|
97
|
+
"@cedarjs/framework-tools": "8.0.0-canary.3526",
|
|
98
98
|
"@prisma/dmmf": "7.10.1",
|
|
99
99
|
"@types/archiver": "^7.0.0",
|
|
100
100
|
"memfs": "4.80.0",
|