create-bw-app 0.25.2 → 0.26.1
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 +1 -1
- package/src/app-manifest.mjs +8 -0
- package/src/constants.mjs +11 -6
- package/src/doctor.mjs +181 -1
- package/src/generator.mjs +29 -9
- package/src/migrations.mjs +116 -6
- package/src/upgrade.mjs +35 -2
- package/template/base/app/(shell)/shell-layout-client.tsx +1 -1
- package/template/base/app/api/organizations/[id]/invitations/[invitationId]/route.ts +8 -0
- package/template/base/app/api/organizations/[id]/invitations/route.ts +2 -2
- package/template/base/app/api/organizations/[id]/route.ts +2 -2
- package/template/base/app/api/organizations/route.ts +2 -2
- package/template/base/config/module-toolbar-controls.tsx +6 -4
- package/template/modules/projects/app/(shell)/projetos/[projectId]/page.tsx +1 -1
- package/template/modules/projects/app/(shell)/projetos/[projectId]/quadro/page.tsx +1 -1
- package/template/modules/projects/app/(shell)/projetos/[projectId]/tarefas/page.tsx +1 -1
- package/template/modules/projects/app/(shell)/projetos/page.tsx +1 -1
- package/template/modules/projects/app/api/account/projects/[id]/route.ts +1 -1
- package/template/modules/projects/app/api/account/projects/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/activity/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/client-access/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/links/[itemId]/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/links/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/members/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/milestones/[itemId]/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/milestones/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/organizations/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/tasks/[itemId]/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/tasks/route.ts +1 -1
- package/template/modules/projects/app/api/projects/organizations/route.ts +1 -1
- package/template/modules/projects/app/api/projects/route.ts +1 -1
- package/template/modules/projects/app/api/projects/setup-options/route.ts +1 -1
- package/template/modules/projects/app/api/projects/stats/route.ts +1 -1
- package/template/supabase/modules/crm/migrations/20260816120000_crm_contact_organizations.sql +102 -0
- package/template/modules/projects/app/(shell)/projetos/projetos-live-mounts.tsx +0 -27
- package/template/modules/projects/app/(shell)/projetos/projetos-server-mounts.tsx +0 -63
- package/template/modules/projects/app/api/projects/_handlers.ts +0 -110
package/package.json
CHANGED
package/src/app-manifest.mjs
CHANGED
|
@@ -129,6 +129,14 @@ export function validateAppManifest(manifest) {
|
|
|
129
129
|
try { normalizeSafeRelativePath(relativePath, `scaffoldFiles path`); } catch (error) { errors.push(error.message); }
|
|
130
130
|
if (!entry || typeof entry.module !== "string" || !/^sha256:[a-f0-9]{64}$/.test(entry.hash || "") || !["current", "drifted", "missing"].includes(entry.status) || (entry.intent != null && !["managed", "owned", "skipped"].includes(entry.intent))) errors.push(`scaffoldFiles.${relativePath} is invalid`);
|
|
131
131
|
}
|
|
132
|
+
if (manifest.migrationDeferrals != null) {
|
|
133
|
+
if (typeof manifest.migrationDeferrals !== "object" || Array.isArray(manifest.migrationDeferrals)) errors.push("migrationDeferrals must be an object");
|
|
134
|
+
else for (const [key, entry] of Object.entries(manifest.migrationDeferrals)) {
|
|
135
|
+
if (!entry || entry.reason !== "destructive" || typeof entry.cursor !== "string" || typeof entry.nextMigration !== "string") {
|
|
136
|
+
errors.push(`migrationDeferrals.${key} is invalid`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
132
140
|
if (manifest.lastDoctor != null && (typeof manifest.lastDoctor.at !== "string" || typeof manifest.lastDoctor.ok !== "boolean")) errors.push("lastDoctor is invalid");
|
|
133
141
|
if (
|
|
134
142
|
manifest.infrastructure != null
|
package/src/constants.mjs
CHANGED
|
@@ -113,8 +113,6 @@ export const MODULE_STARTER_FILES = {
|
|
|
113
113
|
projects: [
|
|
114
114
|
"app/(shell)/projetos/layout.tsx",
|
|
115
115
|
"app/(shell)/projetos/page.tsx",
|
|
116
|
-
"app/(shell)/projetos/projetos-live-mounts.tsx",
|
|
117
|
-
"app/(shell)/projetos/projetos-server-mounts.tsx",
|
|
118
116
|
"app/(shell)/projetos/[projectId]/page.tsx",
|
|
119
117
|
"app/(shell)/projetos/[projectId]/quadro/page.tsx",
|
|
120
118
|
"app/(shell)/projetos/[projectId]/tarefas/page.tsx",
|
|
@@ -122,7 +120,6 @@ export const MODULE_STARTER_FILES = {
|
|
|
122
120
|
"app/(shell)/account/projetos/loading.tsx",
|
|
123
121
|
"app/(shell)/account/projetos/[projectId]/page.tsx",
|
|
124
122
|
"app/(shell)/account/projetos/[projectId]/loading.tsx",
|
|
125
|
-
"app/api/projects/_handlers.ts",
|
|
126
123
|
"app/api/account/projects/route.ts",
|
|
127
124
|
"app/api/account/projects/[id]/route.ts",
|
|
128
125
|
"app/api/dashboard/projects/route.ts",
|
|
@@ -145,6 +142,14 @@ export const MODULE_STARTER_FILES = {
|
|
|
145
142
|
],
|
|
146
143
|
};
|
|
147
144
|
|
|
145
|
+
export const RETIRED_MODULE_STARTER_FILES = {
|
|
146
|
+
projects: [
|
|
147
|
+
"app/(shell)/projetos/projetos-live-mounts.tsx",
|
|
148
|
+
"app/(shell)/projetos/projetos-server-mounts.tsx",
|
|
149
|
+
"app/api/projects/_handlers.ts",
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
|
|
148
153
|
export const PLATFORM_STARTER_FILES = [
|
|
149
154
|
"app/(auth)/auth-provider.tsx",
|
|
150
155
|
"app/(auth)/layout.tsx",
|
|
@@ -185,10 +190,10 @@ export const APP_DEPENDENCY_DEFAULTS = {
|
|
|
185
190
|
"@brightweblabs/core-auth": "^0.12.0",
|
|
186
191
|
"@brightweblabs/infra": "^0.7.0",
|
|
187
192
|
"@brightweblabs/module-admin": "^0.9.4",
|
|
188
|
-
"@brightweblabs/module-crm": "^0.
|
|
193
|
+
"@brightweblabs/module-crm": "^0.18.1",
|
|
189
194
|
"@brightweblabs/module-marketing": "^0.4.24",
|
|
190
|
-
"@brightweblabs/module-orgs": "^0.
|
|
191
|
-
"@brightweblabs/module-projects": "^0.
|
|
195
|
+
"@brightweblabs/module-orgs": "^0.7.0",
|
|
196
|
+
"@brightweblabs/module-projects": "^0.19.0",
|
|
192
197
|
"@brightweblabs/theme": "^0.8.3",
|
|
193
198
|
"@brightweblabs/ui": "^1.5.4",
|
|
194
199
|
"geist": "1.7.2",
|
package/src/doctor.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { stdout as output } from "node:process";
|
|
4
|
-
import {
|
|
4
|
+
import { APP_DEPENDENCY_DEFAULTS, BRIGHTWEB_PACKAGE_NAMES } from "./constants.mjs";
|
|
5
|
+
import { cursorMigrationStatus, exactMigrationCompatibilityStatus } from "./migrations.mjs";
|
|
5
6
|
import { findWorkspaceRoot, loadModuleCatalog, MODULE_PACKAGES, readAppManifest, readConfiguredModuleFlags, satisfiesVersion, validateAppManifest, writeAppManifest } from "./app-manifest.mjs";
|
|
6
7
|
import { loadAppEnvironment, readFirstEnvironmentValue } from "./env.mjs";
|
|
7
8
|
import { pathExists, readJsonIfPresent } from "./generator.mjs";
|
|
@@ -10,6 +11,143 @@ import { scaffoldDrift } from "./scaffold.mjs";
|
|
|
10
11
|
|
|
11
12
|
const HELP = `Usage: bw doctor [options]\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --deployment-url <url> Deployed app URL (defaults to PUBLIC_APP_URL/NEXT_PUBLIC_APP_URL)\n --supabase-region <id> Current Supabase project region override\n --strict Treat warnings as failures\n --report Stamp lastDoctor in the app manifest\n --help Show this help`;
|
|
12
13
|
const RUNTIME_PACKAGE_NAMES = ["react", "react-dom", "next"];
|
|
14
|
+
const LOCAL_DEPENDENCY_PREFIXES = ["file:", "link:", "workspace:", "patch:"];
|
|
15
|
+
|
|
16
|
+
async function findUp(startDir, fileName) {
|
|
17
|
+
let current = path.resolve(startDir);
|
|
18
|
+
while (true) {
|
|
19
|
+
const candidate = path.join(current, fileName);
|
|
20
|
+
if (await pathExists(candidate)) return candidate;
|
|
21
|
+
const parent = path.dirname(current);
|
|
22
|
+
if (parent === current) return null;
|
|
23
|
+
current = parent;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function exactVersionFromDefault(packageName) {
|
|
28
|
+
const requested = APP_DEPENDENCY_DEFAULTS[packageName];
|
|
29
|
+
return typeof requested === "string" ? requested.match(/^(?:\^|~)?(.+)$/)?.[1] ?? null : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function escapeRegExp(value) {
|
|
33
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function lockfileIntegrityForPackage(lockfile, packageName, version) {
|
|
37
|
+
const keyPattern = new RegExp(`^ ['\"]?${escapeRegExp(`${packageName}@${version}`)}['\"]?:$`);
|
|
38
|
+
const lines = lockfile.split("\n");
|
|
39
|
+
const start = lines.findIndex((line) => keyPattern.test(line));
|
|
40
|
+
if (start < 0) return null;
|
|
41
|
+
const block = [];
|
|
42
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
43
|
+
if (/^ \S/.test(lines[index])) break;
|
|
44
|
+
block.push(lines[index]);
|
|
45
|
+
}
|
|
46
|
+
return block.join("\n").match(/^ resolution:\s*\{[^}\n]*integrity:\s*([^,}\s]+)[^}\n]*\}/m)?.[1] ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function lockfileImporterResolution(lockfile, importerKey, packageName) {
|
|
50
|
+
const lines = lockfile.split("\n");
|
|
51
|
+
const importersIndex = lines.findIndex((line) => /^importers:\s*$/.test(line));
|
|
52
|
+
if (importersIndex < 0) return null;
|
|
53
|
+
const normalizedKey = importerKey || ".";
|
|
54
|
+
const importerPattern = new RegExp(`^ ['\"]?${escapeRegExp(normalizedKey)}['\"]?:\\s*$`);
|
|
55
|
+
const importerStart = lines.findIndex((line, index) => index > importersIndex && importerPattern.test(line));
|
|
56
|
+
if (importerStart < 0) return null;
|
|
57
|
+
let importerEnd = lines.length;
|
|
58
|
+
for (let index = importerStart + 1; index < lines.length; index += 1) {
|
|
59
|
+
if (/^ \S/.test(lines[index])) { importerEnd = index; break; }
|
|
60
|
+
}
|
|
61
|
+
const dependencyPattern = new RegExp(`^ ['\"]?${escapeRegExp(packageName)}['\"]?:\\s*$`);
|
|
62
|
+
const dependencyStart = lines.findIndex(
|
|
63
|
+
(line, index) => index > importerStart && index < importerEnd && dependencyPattern.test(line),
|
|
64
|
+
);
|
|
65
|
+
if (dependencyStart < 0) return null;
|
|
66
|
+
const block = [];
|
|
67
|
+
for (let index = dependencyStart + 1; index < importerEnd; index += 1) {
|
|
68
|
+
if (/^ \S/.test(lines[index]) || /^ \S/.test(lines[index])) break;
|
|
69
|
+
block.push(lines[index]);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
specifier: block.join("\n").match(/^ specifier:\s*['\"]?([^'\"\s]+)['\"]?\s*$/m)?.[1] ?? null,
|
|
73
|
+
version: block.join("\n").match(/^ version:\s*['\"]?([^'\"\s]+)['\"]?\s*$/m)?.[1] ?? null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function inspectPackageProvenance(targetDir, dependencyMap) {
|
|
78
|
+
const issues = [];
|
|
79
|
+
const verified = [];
|
|
80
|
+
const lockPath = await findUp(targetDir, "pnpm-lock.yaml");
|
|
81
|
+
const installRoot = path.join(targetDir, "node_modules");
|
|
82
|
+
if (!lockPath || !(await pathExists(installRoot))) {
|
|
83
|
+
return { issues, verified, lockPath, available: false };
|
|
84
|
+
}
|
|
85
|
+
const lockfile = lockPath ? await fs.readFile(lockPath, "utf8") : null;
|
|
86
|
+
const importerKey = lockPath
|
|
87
|
+
? path.relative(path.dirname(lockPath), targetDir).split(path.sep).join("/") || "."
|
|
88
|
+
: ".";
|
|
89
|
+
const workspaceManifest = lockPath ? await readJsonIfPresent(path.join(path.dirname(lockPath), "package.json")) : null;
|
|
90
|
+
const overrideMaps = [
|
|
91
|
+
workspaceManifest?.pnpm?.overrides,
|
|
92
|
+
workspaceManifest?.pnpm?.patchedDependencies,
|
|
93
|
+
workspaceManifest?.overrides,
|
|
94
|
+
workspaceManifest?.resolutions,
|
|
95
|
+
].filter((value) => value && typeof value === "object");
|
|
96
|
+
for (const packageName of BRIGHTWEB_PACKAGE_NAMES) {
|
|
97
|
+
const requested = dependencyMap[packageName];
|
|
98
|
+
if (!requested) continue;
|
|
99
|
+
if (LOCAL_DEPENDENCY_PREFIXES.some((prefix) => String(requested).startsWith(prefix))) {
|
|
100
|
+
issues.push(`${packageName}: local override ${JSON.stringify(requested)} is forbidden`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const overrideKey = overrideMaps.flatMap((overrides) => Object.keys(overrides)).find(
|
|
104
|
+
(key) => key === packageName || key.startsWith(`${packageName}@`),
|
|
105
|
+
);
|
|
106
|
+
if (overrideKey) {
|
|
107
|
+
issues.push(`${packageName}: workspace override or patch ${JSON.stringify(overrideKey)} is forbidden`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const expectedVersion = exactVersionFromDefault(packageName);
|
|
111
|
+
if (!expectedVersion) {
|
|
112
|
+
issues.push(`${packageName}: create-bw-app has no exact compatibility version`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const manifest = await readJsonIfPresent(path.join(targetDir, "node_modules", packageName, "package.json"));
|
|
116
|
+
if (!manifest) {
|
|
117
|
+
issues.push(`${packageName}: installed package manifest is missing`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (manifest.version !== expectedVersion) {
|
|
121
|
+
issues.push(`${packageName}: installed ${manifest.version ?? "unknown"}, expected exact ${expectedVersion}`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!lockfile) {
|
|
125
|
+
issues.push(`${packageName}: pnpm-lock.yaml was not found`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const importerResolution = lockfileImporterResolution(lockfile, importerKey, packageName);
|
|
129
|
+
if (!importerResolution) {
|
|
130
|
+
issues.push(`${packageName}: target importer ${importerKey} has no lockfile resolution`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (LOCAL_DEPENDENCY_PREFIXES.some((prefix) => String(importerResolution.version).startsWith(prefix))) {
|
|
134
|
+
issues.push(`${packageName}: target importer resolves through forbidden ${JSON.stringify(importerResolution.version)}`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const resolvedVersion = importerResolution.version?.split("(")[0] ?? null;
|
|
138
|
+
if (resolvedVersion !== expectedVersion) {
|
|
139
|
+
issues.push(`${packageName}: target importer resolves ${resolvedVersion ?? "unknown"}, expected exact ${expectedVersion}`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const integrity = lockfileIntegrityForPackage(lockfile, packageName, expectedVersion);
|
|
143
|
+
if (!integrity?.startsWith("sha512-")) {
|
|
144
|
+
issues.push(`${packageName}@${expectedVersion}: registry integrity is missing from pnpm-lock.yaml`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
verified.push(`${packageName}@${expectedVersion}`);
|
|
148
|
+
}
|
|
149
|
+
return { issues, verified, lockPath, available: true };
|
|
150
|
+
}
|
|
13
151
|
|
|
14
152
|
function isLocalDeploymentUrl(value) {
|
|
15
153
|
try {
|
|
@@ -202,6 +340,17 @@ export async function doctorBrightwebApp(argvOptions = {}, runtimeOptions = {})
|
|
|
202
340
|
}
|
|
203
341
|
add(packageProblems.length ? "FAIL" : "PASS", "packages", packageProblems.join("; ") || "Installed module packages agree with the manifest.");
|
|
204
342
|
|
|
343
|
+
const provenance = await inspectPackageProvenance(targetDir, dependencyMap);
|
|
344
|
+
if (!provenance.available) {
|
|
345
|
+
add("INFO", "package-provenance", "SKIP exact registry provenance check; install dependencies with a pnpm lockfile first.");
|
|
346
|
+
} else {
|
|
347
|
+
add(
|
|
348
|
+
provenance.issues.length ? "FAIL" : "PASS",
|
|
349
|
+
"package-provenance",
|
|
350
|
+
provenance.issues.join("; ") || `${provenance.verified.length} BrightWeb packages match the exact compatibility set and pnpm registry integrity records.`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
205
354
|
const runtimeVersions = await findInstalledRuntimeVersions(targetDir);
|
|
206
355
|
const duplicateRuntimes = Object.entries(runtimeVersions)
|
|
207
356
|
.filter(([, versions]) => versions.length > 1)
|
|
@@ -276,6 +425,37 @@ export async function doctorBrightwebApp(argvOptions = {}, runtimeOptions = {})
|
|
|
276
425
|
if (status.shipsMigrations && status.missing.length > 0) migrationProblems.push(`${key}: ${status.missing.join(", ")}`);
|
|
277
426
|
}
|
|
278
427
|
add(migrationProblems.length ? "FAIL" : "PASS", "migrations", migrationProblems.join("; ") || "Migration cursors and flattened files agree.");
|
|
428
|
+
for (const key of migrationKeys) {
|
|
429
|
+
const cursor = appManifest.migrationCursor?.[key];
|
|
430
|
+
if (cursor == null && appManifest.adoptionNotes?.allowUncursored) {
|
|
431
|
+
add("WARN", `migration-provenance-${key}`, `${key}: exact migration provenance is unavailable because uncursored adoption is enabled.`);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const status = await exactMigrationCompatibilityStatus({
|
|
435
|
+
targetDir,
|
|
436
|
+
moduleKey: key,
|
|
437
|
+
cursor,
|
|
438
|
+
catalogEntry: catalog[key],
|
|
439
|
+
allowDeferred: appManifest.migrationDeferrals?.[key]?.reason === "destructive"
|
|
440
|
+
&& appManifest.migrationDeferrals[key].cursor === cursor,
|
|
441
|
+
});
|
|
442
|
+
if (!status.shipsMigrations) continue;
|
|
443
|
+
const deferral = appManifest.migrationDeferrals?.[key];
|
|
444
|
+
if (deferral && (!status.deferred?.length || deferral.nextMigration !== status.deferred[0] || !status.nextDeferredIsDestructive)) {
|
|
445
|
+
status.issues.push(`recorded destructive deferral ${deferral.nextMigration} does not match the next destructive package migration`);
|
|
446
|
+
}
|
|
447
|
+
add(
|
|
448
|
+
status.issues.length ? "FAIL" : "PASS",
|
|
449
|
+
`migration-provenance-${key}`,
|
|
450
|
+
status.issues.join("; ") || `${status.verified.length} ${key} migration files match compatible package provenance, source filename, current cursor, and sha256 content.`,
|
|
451
|
+
);
|
|
452
|
+
if (status.deferred?.length && status.issues.length === 0) {
|
|
453
|
+
add("INFO", `migration-deferred-${key}`, `${key}: ${status.deferred.length} later package migration${status.deferred.length === 1 ? " is" : "s are"} intentionally outside cursor ${cursor}: ${status.deferred.join(", ")}.`);
|
|
454
|
+
}
|
|
455
|
+
if (status.legacyEquivalent?.length) {
|
|
456
|
+
add("INFO", `migration-legacy-equivalent-${key}`, `${key}: ${status.legacyEquivalent.length} immutable historical migration file${status.legacyEquivalent.length === 1 ? " matches" : "s match"} a reviewed SQL-equivalent legacy hash.`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
279
459
|
add("WARN", "db-objects", "SKIP live database checks are not available yet.");
|
|
280
460
|
return finish(checks, argvOptions, appManifest, targetDir);
|
|
281
461
|
}
|
package/src/generator.mjs
CHANGED
|
@@ -1044,7 +1044,7 @@ export function createModuleToolbarControlsConfig(selectedModules) {
|
|
|
1044
1044
|
}
|
|
1045
1045
|
if (selectedModules.includes("projects")) {
|
|
1046
1046
|
imports.push('import { ProjectBoardToolbarControls, ProjectsToolbarControls } from "@brightweblabs/module-projects/ui";');
|
|
1047
|
-
entries.push(' projects: (
|
|
1047
|
+
entries.push(' projects: (viewer) => <ProjectsToolbarControls viewer={viewer} />,');
|
|
1048
1048
|
entries.push(' "project-board": () => <ProjectBoardToolbarControls />,');
|
|
1049
1049
|
}
|
|
1050
1050
|
if (selectedModules.includes("marketing")) {
|
|
@@ -1060,13 +1060,15 @@ export function createModuleToolbarControlsConfig(selectedModules) {
|
|
|
1060
1060
|
"// MANAGED BY BRIGHTWEB — regenerated when modules are added, removed, or updated.",
|
|
1061
1061
|
...imports,
|
|
1062
1062
|
"",
|
|
1063
|
-
"
|
|
1063
|
+
"type ModuleToolbarViewer = { isAdmin: boolean };",
|
|
1064
|
+
"",
|
|
1065
|
+
"const toolbarControlBySurface: Partial<Record<ShellToolbarSurface, (viewer: ModuleToolbarViewer) => ReactNode>> = {",
|
|
1064
1066
|
...entries,
|
|
1065
1067
|
"};",
|
|
1066
1068
|
"",
|
|
1067
|
-
"export function getModuleToolbarControls(pathname: string, toolbarRoutes: ShellToolbarRouteConfig[],
|
|
1069
|
+
"export function getModuleToolbarControls(pathname: string, toolbarRoutes: ShellToolbarRouteConfig[], viewer: ModuleToolbarViewer) {",
|
|
1068
1070
|
" const surface = resolveShellToolbarSurface(pathname, toolbarRoutes);",
|
|
1069
|
-
" return surface ? toolbarControlBySurface[surface]?.(
|
|
1071
|
+
" return surface ? toolbarControlBySurface[surface]?.(viewer) ?? null : null;",
|
|
1070
1072
|
"}",
|
|
1071
1073
|
"",
|
|
1072
1074
|
].join("\n");
|
|
@@ -1074,11 +1076,11 @@ export function createModuleToolbarControlsConfig(selectedModules) {
|
|
|
1074
1076
|
|
|
1075
1077
|
function createOrganizationRoute(methods, enabled) {
|
|
1076
1078
|
const lines = ['export const dynamic = "force-dynamic";', ""];
|
|
1077
|
-
for (const { method, handler, context = false } of methods) {
|
|
1079
|
+
for (const { method, handler, context = false, packageName = "@brightweblabs/module-orgs" } of methods) {
|
|
1078
1080
|
if (enabled) {
|
|
1079
1081
|
lines.push(
|
|
1080
1082
|
`export async function ${method}(request: Request${context ? ', context: { params: Promise<{ id: string }> }' : ""}) {`,
|
|
1081
|
-
` const { ${handler} } = await import("
|
|
1083
|
+
` const { ${handler} } = await import("${packageName}");`,
|
|
1082
1084
|
` return ${handler}(request${context ? ", context" : ""});`,
|
|
1083
1085
|
"}",
|
|
1084
1086
|
"",
|
|
@@ -1214,20 +1216,34 @@ export function createOptionalModuleRouteFiles(selectedModules) {
|
|
|
1214
1216
|
return {
|
|
1215
1217
|
"app/api/invitations/_dependencies.ts": invitationDependencies,
|
|
1216
1218
|
"app/api/organizations/route.ts": createOrganizationRoute([
|
|
1217
|
-
|
|
1219
|
+
crmEnabled
|
|
1220
|
+
? { method: "POST", handler: "handleCrmOrganizationsPostRequest", packageName: "@brightweblabs/module-crm" }
|
|
1221
|
+
: { method: "POST", handler: "handleOrganizationsPostRequest" },
|
|
1218
1222
|
], orgsEnabled),
|
|
1219
1223
|
"app/api/organizations/[id]/route.ts": createOrganizationRoute([
|
|
1220
|
-
|
|
1224
|
+
crmEnabled
|
|
1225
|
+
? { method: "PATCH", handler: "handleCrmOrganizationPatchRequest", packageName: "@brightweblabs/module-crm", context: true }
|
|
1226
|
+
: { method: "PATCH", handler: "handleOrganizationPatchRequest", context: true },
|
|
1221
1227
|
{ method: "DELETE", handler: "handleOrganizationDeleteRequest", context: true },
|
|
1222
1228
|
], orgsEnabled),
|
|
1223
1229
|
"app/api/organizations/[id]/invitations/route.ts": createOrganizationRoute([
|
|
1224
1230
|
{ method: "GET", handler: "handleOrganizationInvitationsGetRequest", context: true },
|
|
1225
|
-
|
|
1231
|
+
crmEnabled
|
|
1232
|
+
? { method: "POST", handler: "handleCrmOrganizationInvitationsPostRequest", packageName: "@brightweblabs/module-crm", context: true }
|
|
1233
|
+
: { method: "POST", handler: "handleOrganizationInvitationsPostRequest", context: true },
|
|
1226
1234
|
], orgsEnabled),
|
|
1227
1235
|
"app/api/organizations/[id]/invitations/[invitationId]/route.ts": orgsEnabled
|
|
1228
1236
|
? [
|
|
1229
1237
|
'export const dynamic = "force-dynamic";',
|
|
1230
1238
|
"",
|
|
1239
|
+
"export async function POST(",
|
|
1240
|
+
" request: Request,",
|
|
1241
|
+
" context: { params: Promise<{ id: string; invitationId: string }> },",
|
|
1242
|
+
") {",
|
|
1243
|
+
' const { handleOrganizationInvitationResendRequest } = await import("@brightweblabs/module-orgs");',
|
|
1244
|
+
" return handleOrganizationInvitationResendRequest(request, context);",
|
|
1245
|
+
"}",
|
|
1246
|
+
"",
|
|
1231
1247
|
"export async function DELETE(",
|
|
1232
1248
|
" request: Request,",
|
|
1233
1249
|
" context: { params: Promise<{ id: string; invitationId: string }> },",
|
|
@@ -1242,6 +1258,10 @@ export function createOptionalModuleRouteFiles(selectedModules) {
|
|
|
1242
1258
|
"",
|
|
1243
1259
|
'type RouteContext = { params: Promise<{ id: string; invitationId: string }> };',
|
|
1244
1260
|
"",
|
|
1261
|
+
"export async function POST(_request: Request, _context: RouteContext) {",
|
|
1262
|
+
" return new Response(null, { status: 404 });",
|
|
1263
|
+
"}",
|
|
1264
|
+
"",
|
|
1245
1265
|
"export async function DELETE(_request: Request, _context: RouteContext) {",
|
|
1246
1266
|
" return new Response(null, { status: 404 });",
|
|
1247
1267
|
"}",
|
package/src/migrations.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import { resolveSafeRelativePath } from "./safe-path.mjs";
|
|
4
5
|
import { TEMPLATE_ROOT, pathExists } from "./generator.mjs";
|
|
5
6
|
|
|
@@ -49,13 +50,17 @@ export async function planMigrationAppends({
|
|
|
49
50
|
}) {
|
|
50
51
|
const migrationsDir = await findAppMigrationsDirectory(targetDir);
|
|
51
52
|
const existing = (await pathExists(migrationsDir))
|
|
52
|
-
? (await fs.readdir(migrationsDir)).filter((fileName) => fileName.endsWith(".sql")).sort()
|
|
53
|
+
? await Promise.all((await fs.readdir(migrationsDir)).filter((fileName) => fileName.endsWith(".sql")).sort().map(async (fileName) => ({
|
|
54
|
+
fileName,
|
|
55
|
+
targetPath: path.join(migrationsDir, fileName),
|
|
56
|
+
content: await fs.readFile(path.join(migrationsDir, fileName), "utf8"),
|
|
57
|
+
})))
|
|
53
58
|
: [];
|
|
54
|
-
let sequence = existing.reduce((maximum,
|
|
55
|
-
const match = fileName.match(/^(\d+)_/);
|
|
59
|
+
let sequence = existing.reduce((maximum, entry) => {
|
|
60
|
+
const match = entry.fileName.match(/^(\d+)_/);
|
|
56
61
|
return Math.max(maximum, Number(match?.[1] || 0));
|
|
57
62
|
}, 0);
|
|
58
|
-
const
|
|
63
|
+
const appends = [];
|
|
59
64
|
const deferred = [];
|
|
60
65
|
const nextCursor = { ...migrationCursor };
|
|
61
66
|
for (const moduleKey of moduleKeys) {
|
|
@@ -88,7 +93,7 @@ export async function planMigrationAppends({
|
|
|
88
93
|
const targetFileName = `${String(sequence).padStart(4, "0")}_${moduleKey}__${entry.fileName}`;
|
|
89
94
|
const source = await fs.readFile(entry.sourcePath, "utf8");
|
|
90
95
|
const version = catalog[moduleKey]?.version || "unknown";
|
|
91
|
-
|
|
96
|
+
appends.push({
|
|
92
97
|
moduleKey,
|
|
93
98
|
originalFileName: entry.fileName,
|
|
94
99
|
targetFileName,
|
|
@@ -98,7 +103,7 @@ export async function planMigrationAppends({
|
|
|
98
103
|
}
|
|
99
104
|
if (migrationsInScope.length > 0) nextCursor[moduleKey] = migrationsInScope.at(-1).fileName;
|
|
100
105
|
}
|
|
101
|
-
return { writes, deferred, nextCursor };
|
|
106
|
+
return { writes: appends, repairs: [], appends, deferred, nextCursor };
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
export async function applyMigrationWrites(writes) {
|
|
@@ -131,3 +136,108 @@ export async function cursorMigrationStatus({ targetDir, moduleKey, cursor, cata
|
|
|
131
136
|
})).map((entry) => entry.fileName),
|
|
132
137
|
};
|
|
133
138
|
}
|
|
139
|
+
|
|
140
|
+
function sha256(source) {
|
|
141
|
+
return createHash("sha256").update(source).digest("hex");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Historical generated files are immutable. These hashes are reviewed SQL-equivalent
|
|
145
|
+
// renderings shipped by early consumers before canonical provenance was enforced.
|
|
146
|
+
const REVIEWED_LEGACY_MIGRATION_HASHES = new Map([
|
|
147
|
+
["core/20260731120000_core_notifications.sql", new Set(["20fbd2158741871ac6b65c4033db5d7c9e889b03a5876d1f32f2b368b35cf24a"])],
|
|
148
|
+
["projects/20260731121000_project_notification_audiences.sql", new Set(["1726f2f2403401cda677419e26b49ffa521e49ef0bfa72b56e42428ded7c6312"])],
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
function compareSemver(left, right) {
|
|
152
|
+
const leftParts = String(left).split(".").map((value) => Number.parseInt(value, 10));
|
|
153
|
+
const rightParts = String(right).split(".").map((value) => Number.parseInt(value, 10));
|
|
154
|
+
if (leftParts.length !== 3 || rightParts.length !== 3 || [...leftParts, ...rightParts].some(Number.isNaN)) return null;
|
|
155
|
+
for (let index = 0; index < 3; index += 1) {
|
|
156
|
+
if (leftParts[index] !== rightParts[index]) return leftParts[index] < rightParts[index] ? -1 : 1;
|
|
157
|
+
}
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function exactMigrationCompatibilityStatus({ targetDir, moduleKey, cursor, catalogEntry, allowDeferred = false }) {
|
|
162
|
+
const migrations = await getModuleMigrations(moduleKey, catalogEntry);
|
|
163
|
+
if (migrations.length === 0) return { shipsMigrations: false, issues: [], verified: [] };
|
|
164
|
+
const issues = [];
|
|
165
|
+
const latest = migrations.at(-1)?.fileName ?? null;
|
|
166
|
+
let migrationsInScope = [];
|
|
167
|
+
if (!cursor) {
|
|
168
|
+
issues.push("migration cursor is missing");
|
|
169
|
+
} else if (!migrations.some((entry) => entry.fileName === cursor)) {
|
|
170
|
+
issues.push(`cursor ${cursor} does not exist in the shipped migration history`);
|
|
171
|
+
} else {
|
|
172
|
+
migrationsInScope = migrations.filter((entry) => entry.fileName <= cursor);
|
|
173
|
+
if (cursor !== latest && !allowDeferred) {
|
|
174
|
+
issues.push(`cursor ${cursor} is stale; exact package compatibility requires ${latest}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const migrationsDir = await findAppMigrationsDirectory(targetDir);
|
|
179
|
+
const installed = [];
|
|
180
|
+
if (await pathExists(migrationsDir)) {
|
|
181
|
+
for (const fileName of await fs.readdir(migrationsDir)) {
|
|
182
|
+
if (!fileName.endsWith(".sql")) continue;
|
|
183
|
+
installed.push({ fileName, content: await fs.readFile(path.join(migrationsDir, fileName), "utf8") });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const verified = [];
|
|
188
|
+
const legacyEquivalent = [];
|
|
189
|
+
for (const entry of migrationsInScope) {
|
|
190
|
+
const matches = installed.filter(({ fileName, content }) => {
|
|
191
|
+
const header = content.match(/^\s*--\s*bw-module:\s*([^@\s]+)@([^\s]+)\s+([^\s]+)\s*\n/im);
|
|
192
|
+
return (header?.[1] === moduleKey && header?.[3] === entry.fileName)
|
|
193
|
+
|| fileName === entry.fileName
|
|
194
|
+
|| fileName.endsWith(`_${moduleKey}__${entry.fileName}`);
|
|
195
|
+
});
|
|
196
|
+
if (matches.length === 0) {
|
|
197
|
+
issues.push(`${entry.fileName}: generated migration file is missing`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (matches.length > 1) {
|
|
201
|
+
issues.push(`${entry.fileName}: appears in multiple generated migration files (${matches.map(({ fileName }) => fileName).join(", ")})`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const [{ fileName, content }] = matches;
|
|
205
|
+
const header = content.match(/^\s*--\s*bw-module:\s*([^@\s]+)@([^\s]+)\s+([^\s]+)\s*\n/im);
|
|
206
|
+
const expectedVersion = catalogEntry?.version;
|
|
207
|
+
if (expectedVersion && header) {
|
|
208
|
+
const comparison = compareSemver(header[2], expectedVersion);
|
|
209
|
+
if (comparison == null || comparison > 0) {
|
|
210
|
+
issues.push(`${fileName}: provenance version ${header[2]} is incompatible with installed ${moduleKey}@${expectedVersion}`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const source = await fs.readFile(entry.sourcePath, "utf8");
|
|
215
|
+
const generatedSource = header ? content.slice(header[0].length) : content;
|
|
216
|
+
const expectedHash = sha256(source);
|
|
217
|
+
const actualHash = sha256(generatedSource);
|
|
218
|
+
if (actualHash !== expectedHash) {
|
|
219
|
+
if (REVIEWED_LEGACY_MIGRATION_HASHES.get(`${moduleKey}/${entry.fileName}`)?.has(actualHash)) {
|
|
220
|
+
verified.push({ fileName, originalFileName: entry.fileName, sha256: actualHash });
|
|
221
|
+
legacyEquivalent.push({ fileName, originalFileName: entry.fileName, sha256: actualHash, canonicalSha256: expectedHash });
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
issues.push(`${fileName}: sha256 ${actualHash} does not match ${entry.fileName} sha256 ${expectedHash}`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
verified.push({ fileName, originalFileName: entry.fileName, sha256: expectedHash });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const deferredEntries = cursor && migrations.some((entry) => entry.fileName === cursor)
|
|
231
|
+
? migrations.filter((entry) => entry.fileName > cursor)
|
|
232
|
+
: [];
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
shipsMigrations: true,
|
|
236
|
+
latest,
|
|
237
|
+
issues,
|
|
238
|
+
verified,
|
|
239
|
+
legacyEquivalent,
|
|
240
|
+
deferred: deferredEntries.map((entry) => entry.fileName),
|
|
241
|
+
nextDeferredIsDestructive: deferredEntries[0]?.destructive === true,
|
|
242
|
+
};
|
|
243
|
+
}
|
package/src/upgrade.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { pathExists, runInstall } from "./generator.mjs";
|
|
|
6
6
|
import { applyMigrationWrites, getModuleMigrations, planMigrationAppends } from "./migrations.mjs";
|
|
7
7
|
import { buildBrightwebAppUpdatePlan } from "./update.mjs";
|
|
8
8
|
import { resolveSafeRelativePath } from "./safe-path.mjs";
|
|
9
|
+
import { RETIRED_MODULE_STARTER_FILES } from "./constants.mjs";
|
|
9
10
|
|
|
10
11
|
const HELP = `Usage: bw upgrade [moduleKey] [options]\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --through-migration <file> Advance the named module only through this migration\n --include-destructive-migrations Explicitly include held destructive migrations\n --allow-stale-fallback Use baked-in versions if npm lookup fails\n --install Install changed dependencies\n --refresh-starters Refresh unchanged starter files\n --dry-run Print the upgrade plan without writing\n --help Show this help`;
|
|
11
12
|
|
|
@@ -40,6 +41,19 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
|
|
|
40
41
|
if (await hashFile(filePath) !== record.hash) drifted.push(relativePath);
|
|
41
42
|
}
|
|
42
43
|
const protectedPaths = new Set([...drifted, ...intentional]);
|
|
44
|
+
const obsoleteScaffoldFiles = [];
|
|
45
|
+
if (argvOptions.refreshStarters) {
|
|
46
|
+
for (const moduleKey of Object.keys(appManifest.modules)) {
|
|
47
|
+
for (const relativePath of RETIRED_MODULE_STARTER_FILES[moduleKey] ?? []) {
|
|
48
|
+
const record = appManifest.scaffoldFiles[relativePath];
|
|
49
|
+
if (!record || protectedPaths.has(relativePath)) continue;
|
|
50
|
+
const targetPath = resolveSafeRelativePath(targetDir, relativePath, "Obsolete scaffold file path");
|
|
51
|
+
if (!(await pathExists(targetPath)) || await hashFile(targetPath) !== record.hash) continue;
|
|
52
|
+
obsoleteScaffoldFiles.push({ relativePath, targetPath });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
plan.fileDeletes = obsoleteScaffoldFiles;
|
|
43
57
|
plan.fileWrites = plan.fileWrites.filter((entry) => entry.type !== "starter" || !protectedPaths.has(entry.relativePath));
|
|
44
58
|
plan.starterFilesToRefresh = plan.fileWrites.filter((entry) => entry.type === "starter").map((entry) => entry.relativePath);
|
|
45
59
|
plan.starterFilesDrifted = Array.from(new Set([...plan.starterFilesDrifted, ...drifted]));
|
|
@@ -93,7 +107,7 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
|
|
|
93
107
|
migrationCursor: appManifest.migrationCursor,
|
|
94
108
|
migrationUpperBounds,
|
|
95
109
|
});
|
|
96
|
-
output.write(`bw upgrade\nPackages to update: ${plan.packageUpdates.length}\nManaged files to write: ${plan.fileWrites.length}\nMigrations to append: ${migrationPlan.
|
|
110
|
+
output.write(`bw upgrade\nPackages to update: ${plan.packageUpdates.length}\nManaged files to write: ${plan.fileWrites.length}\nObsolete managed files to remove: ${plan.fileDeletes.length}\nMigrations to append: ${migrationPlan.appends.length}\n`);
|
|
97
111
|
if (throughMigration) output.write(`Migration cutoff: ${moduleKey} through ${throughMigration}\n`);
|
|
98
112
|
for (const boundary of safetyBoundaries) {
|
|
99
113
|
output.write(`Migration safety boundary: ${boundary.moduleKey} through ${boundary.boundary}; held ${boundary.destructiveMigration}\n`);
|
|
@@ -107,14 +121,33 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
|
|
|
107
121
|
for (const relativePath of missing) output.write(`- missing: ${relativePath}\n`);
|
|
108
122
|
for (const relativePath of drifted) output.write(`- drifted: ${relativePath}\n`);
|
|
109
123
|
for (const relativePath of intentional) output.write(`- intent-protected: ${relativePath}\n`);
|
|
124
|
+
for (const entry of plan.fileDeletes) output.write(`- obsolete managed file: ${entry.relativePath}\n`);
|
|
110
125
|
if (argvOptions.dryRun) return { dryRun: true, plan, migrationPlan, drifted, missing };
|
|
111
126
|
|
|
112
127
|
for (const write of plan.fileWrites) {
|
|
113
128
|
await fs.mkdir(path.dirname(write.targetPath), { recursive: true });
|
|
114
129
|
await fs.writeFile(write.targetPath, write.content, "utf8");
|
|
115
130
|
}
|
|
131
|
+
for (const deletion of plan.fileDeletes) {
|
|
132
|
+
await fs.unlink(deletion.targetPath);
|
|
133
|
+
delete appManifest.scaffoldFiles[deletion.relativePath];
|
|
134
|
+
}
|
|
116
135
|
await applyMigrationWrites(migrationPlan.writes);
|
|
117
136
|
appManifest.migrationCursor = migrationPlan.nextCursor;
|
|
137
|
+
appManifest.migrationDeferrals ??= {};
|
|
138
|
+
for (const key of moduleKeys) {
|
|
139
|
+
const firstDeferred = migrationPlan.deferred.find((entry) => entry.moduleKey === key);
|
|
140
|
+
if (firstDeferred?.destructive) {
|
|
141
|
+
appManifest.migrationDeferrals[key] = {
|
|
142
|
+
reason: "destructive",
|
|
143
|
+
cursor: migrationPlan.nextCursor[key],
|
|
144
|
+
nextMigration: firstDeferred.fileName,
|
|
145
|
+
};
|
|
146
|
+
} else {
|
|
147
|
+
delete appManifest.migrationDeferrals[key];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (Object.keys(appManifest.migrationDeferrals).length === 0) delete appManifest.migrationDeferrals;
|
|
118
151
|
for (const [key, entry] of Object.entries(appManifest.modules)) {
|
|
119
152
|
const packageName = catalog[key]?.packageName;
|
|
120
153
|
if (catalog[key]?.packageRoot || plan.targetVersions?.[packageName]) entry.version = catalog[key].version;
|
|
@@ -144,7 +177,7 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
|
|
|
144
177
|
const runner = runtimeOptions.installRunner || runInstall;
|
|
145
178
|
await runner(plan.packageManager, plan.dependencyMode === "workspace" && plan.workspaceRoot ? plan.workspaceRoot : targetDir);
|
|
146
179
|
}
|
|
147
|
-
output.write(`Applied ${plan.fileWrites.length} managed change${plan.fileWrites.length === 1 ? "" : "s"} and ${migrationPlan.
|
|
180
|
+
output.write(`Applied ${plan.fileWrites.length} managed change${plan.fileWrites.length === 1 ? "" : "s"}, removed ${plan.fileDeletes.length} obsolete managed file${plan.fileDeletes.length === 1 ? "" : "s"}, and added ${migrationPlan.appends.length} new migration${migrationPlan.appends.length === 1 ? "" : "s"}.\n`);
|
|
148
181
|
return { dryRun: false, plan, migrationPlan, drifted, missing };
|
|
149
182
|
}
|
|
150
183
|
|
|
@@ -84,7 +84,7 @@ function ShellLayoutInner({
|
|
|
84
84
|
viewer.email ||
|
|
85
85
|
"Conta";
|
|
86
86
|
const projectsBaseHref = pathname.startsWith("/projects") ? "/projects" : "/projetos";
|
|
87
|
-
const toolbarControls = getModuleToolbarControls(pathname, toolbarRoutes, viewer
|
|
87
|
+
const toolbarControls = getModuleToolbarControls(pathname, toolbarRoutes, viewer);
|
|
88
88
|
|
|
89
89
|
const dispatchShellAction = useShellActionDispatch();
|
|
90
90
|
useShellAction("projects-back-to-portfolio", () => {
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
2
|
|
|
3
|
+
export async function POST(
|
|
4
|
+
request: Request,
|
|
5
|
+
context: { params: Promise<{ id: string; invitationId: string }> },
|
|
6
|
+
) {
|
|
7
|
+
const { handleOrganizationInvitationResendRequest } = await import("@brightweblabs/module-orgs");
|
|
8
|
+
return handleOrganizationInvitationResendRequest(request, context);
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
export async function DELETE(
|
|
4
12
|
request: Request,
|
|
5
13
|
context: { params: Promise<{ id: string; invitationId: string }> },
|
|
@@ -6,6 +6,6 @@ export async function GET(request: Request, context: { params: Promise<{ id: str
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export async function POST(request: Request, context: { params: Promise<{ id: string }> }) {
|
|
9
|
-
const {
|
|
10
|
-
return
|
|
9
|
+
const { handleCrmOrganizationInvitationsPostRequest } = await import("@brightweblabs/module-crm");
|
|
10
|
+
return handleCrmOrganizationInvitationsPostRequest(request, context);
|
|
11
11
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
2
|
|
|
3
3
|
export async function PATCH(request: Request, context: { params: Promise<{ id: string }> }) {
|
|
4
|
-
const {
|
|
5
|
-
return
|
|
4
|
+
const { handleCrmOrganizationPatchRequest } = await import("@brightweblabs/module-crm");
|
|
5
|
+
return handleCrmOrganizationPatchRequest(request, context);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export async function DELETE(request: Request, context: { params: Promise<{ id: string }> }) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
2
|
|
|
3
3
|
export async function POST(request: Request) {
|
|
4
|
-
const {
|
|
5
|
-
return
|
|
4
|
+
const { handleCrmOrganizationsPostRequest } = await import("@brightweblabs/module-crm");
|
|
5
|
+
return handleCrmOrganizationsPostRequest(request);
|
|
6
6
|
}
|
|
@@ -8,16 +8,18 @@ import { ProjectBoardToolbarControls, ProjectsToolbarControls } from "@brightweb
|
|
|
8
8
|
import { MarketingToolbarControls } from "@brightweblabs/module-marketing/ui";
|
|
9
9
|
|
|
10
10
|
// MANAGED BY BRIGHTWEB — regenerated when modules are added, removed, or updated.
|
|
11
|
-
|
|
11
|
+
type ModuleToolbarViewer = { isAdmin: boolean };
|
|
12
|
+
|
|
13
|
+
const toolbarControlBySurface: Partial<Record<ShellToolbarSurface, (viewer: ModuleToolbarViewer) => ReactNode>> = {
|
|
12
14
|
"admin-users": () => <AdminToolbarControls />,
|
|
13
15
|
crm: () => <CrmToolbarControls />,
|
|
14
16
|
"crm-organizations": () => <CrmOrganizationsToolbarControls />,
|
|
15
|
-
projects: (
|
|
17
|
+
projects: (viewer) => <ProjectsToolbarControls viewer={viewer} />,
|
|
16
18
|
"project-board": () => <ProjectBoardToolbarControls />,
|
|
17
19
|
marketing: () => <MarketingToolbarControls />,
|
|
18
20
|
};
|
|
19
21
|
|
|
20
|
-
export function getModuleToolbarControls(pathname: string, toolbarRoutes: ShellToolbarRouteConfig[],
|
|
22
|
+
export function getModuleToolbarControls(pathname: string, toolbarRoutes: ShellToolbarRouteConfig[], viewer: ModuleToolbarViewer) {
|
|
21
23
|
const surface = resolveShellToolbarSurface(pathname, toolbarRoutes);
|
|
22
|
-
return surface ? toolbarControlBySurface[surface]?.(
|
|
24
|
+
return surface ? toolbarControlBySurface[surface]?.(viewer) ?? null : null;
|
|
23
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { ProjectDetailPortugueseRoutePage as default } from "@brightweblabs/module-projects/routes";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { ProjectBoardPortugueseRoutePage as default } from "@brightweblabs/module-projects/routes";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { ProjectTasksPortugueseRoutePage as default } from "@brightweblabs/module-projects/routes";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { ProjectsPortugueseRoutePage as default } from "@brightweblabs/module-projects/routes";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { clientProjectDetailGet as GET } from "
|
|
2
|
+
export { clientProjectDetailGet as GET } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { clientProjectsGet as GET } from "
|
|
2
|
+
export { clientProjectsGet as GET } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectActivityGet as GET } from "
|
|
2
|
+
export { projectActivityGet as GET } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectClientAccessGet as GET, projectClientAccessPatch as PATCH } from "
|
|
2
|
+
export { projectClientAccessGet as GET, projectClientAccessPatch as PATCH } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectLinkPatch as PATCH, projectLinkDelete as DELETE } from "
|
|
2
|
+
export { projectLinkPatch as PATCH, projectLinkDelete as DELETE } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectLinksPost as POST } from "
|
|
2
|
+
export { projectLinksPost as POST } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectMembersGet as GET, projectMembersPut as PUT } from "
|
|
2
|
+
export { projectMembersGet as GET, projectMembersPut as PUT } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectMilestonePatch as PATCH, projectMilestoneDelete as DELETE } from "
|
|
2
|
+
export { projectMilestonePatch as PATCH, projectMilestoneDelete as DELETE } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectMilestonesPost as POST } from "
|
|
2
|
+
export { projectMilestonesPost as POST } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectOrganizationsPatch as PATCH } from "
|
|
2
|
+
export { projectOrganizationsPatch as PATCH } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectDashboardGet as GET, projectPatch as PATCH, projectDelete as DELETE } from "
|
|
2
|
+
export { projectDashboardGet as GET, projectPatch as PATCH, projectDelete as DELETE } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectTaskPatch as PATCH, projectTaskDelete as DELETE } from "
|
|
2
|
+
export { projectTaskPatch as PATCH, projectTaskDelete as DELETE } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectTasksPost as POST } from "
|
|
2
|
+
export { projectTasksPost as POST } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectsOrganizationsGet as GET, projectsOrganizationsPost as POST } from "
|
|
2
|
+
export { projectsOrganizationsGet as GET, projectsOrganizationsPost as POST } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectsGet as GET, projectsPost as POST } from "
|
|
2
|
+
export { projectsGet as GET, projectsPost as POST } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectsSetupOptionsGet as GET } from "
|
|
2
|
+
export { projectsSetupOptionsGet as GET } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectsStatsGet as GET } from "
|
|
2
|
+
export { projectsStatsGet as GET } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
-- Compatibility-first multi-organization CRM contacts.
|
|
2
|
+
-- crm_contacts.organization_id remains the primary organization projection while
|
|
3
|
+
-- this link table records every organization relationship.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE IF NOT EXISTS public.crm_contact_organizations (
|
|
6
|
+
contact_id uuid NOT NULL REFERENCES public.crm_contacts(id) ON DELETE CASCADE,
|
|
7
|
+
organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
|
|
8
|
+
is_primary boolean NOT NULL DEFAULT false,
|
|
9
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
10
|
+
PRIMARY KEY (contact_id, organization_id)
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE UNIQUE INDEX IF NOT EXISTS crm_contact_organizations_one_primary_idx
|
|
14
|
+
ON public.crm_contact_organizations (contact_id)
|
|
15
|
+
WHERE is_primary;
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS crm_contact_organizations_organization_idx
|
|
18
|
+
ON public.crm_contact_organizations (organization_id, contact_id);
|
|
19
|
+
|
|
20
|
+
INSERT INTO public.crm_contact_organizations (contact_id, organization_id, is_primary)
|
|
21
|
+
SELECT contact.id, contact.organization_id, true
|
|
22
|
+
FROM public.crm_contacts contact
|
|
23
|
+
WHERE contact.organization_id IS NOT NULL
|
|
24
|
+
ON CONFLICT (contact_id, organization_id) DO UPDATE SET is_primary = true;
|
|
25
|
+
|
|
26
|
+
CREATE OR REPLACE FUNCTION public.sync_crm_contact_primary_organization_link()
|
|
27
|
+
RETURNS trigger
|
|
28
|
+
LANGUAGE plpgsql
|
|
29
|
+
SECURITY DEFINER
|
|
30
|
+
SET search_path = public
|
|
31
|
+
AS $$
|
|
32
|
+
BEGIN
|
|
33
|
+
UPDATE public.crm_contact_organizations
|
|
34
|
+
SET is_primary = false
|
|
35
|
+
WHERE contact_id = NEW.id
|
|
36
|
+
AND is_primary;
|
|
37
|
+
|
|
38
|
+
IF NEW.organization_id IS NOT NULL THEN
|
|
39
|
+
INSERT INTO public.crm_contact_organizations (contact_id, organization_id, is_primary)
|
|
40
|
+
VALUES (NEW.id, NEW.organization_id, true)
|
|
41
|
+
ON CONFLICT (contact_id, organization_id)
|
|
42
|
+
DO UPDATE SET is_primary = true;
|
|
43
|
+
END IF;
|
|
44
|
+
|
|
45
|
+
RETURN NEW;
|
|
46
|
+
END;
|
|
47
|
+
$$;
|
|
48
|
+
|
|
49
|
+
DROP TRIGGER IF EXISTS trg_sync_crm_contact_primary_organization_link ON public.crm_contacts;
|
|
50
|
+
CREATE TRIGGER trg_sync_crm_contact_primary_organization_link
|
|
51
|
+
AFTER INSERT OR UPDATE OF organization_id ON public.crm_contacts
|
|
52
|
+
FOR EACH ROW
|
|
53
|
+
EXECUTE FUNCTION public.sync_crm_contact_primary_organization_link();
|
|
54
|
+
|
|
55
|
+
CREATE OR REPLACE FUNCTION public.link_crm_contact_organization(
|
|
56
|
+
p_contact_id uuid,
|
|
57
|
+
p_organization_id uuid
|
|
58
|
+
)
|
|
59
|
+
RETURNS void
|
|
60
|
+
LANGUAGE plpgsql
|
|
61
|
+
SECURITY DEFINER
|
|
62
|
+
SET search_path = public
|
|
63
|
+
AS $$
|
|
64
|
+
DECLARE
|
|
65
|
+
v_primary_organization_id uuid;
|
|
66
|
+
BEGIN
|
|
67
|
+
SELECT organization_id
|
|
68
|
+
INTO v_primary_organization_id
|
|
69
|
+
FROM public.crm_contacts
|
|
70
|
+
WHERE id = p_contact_id
|
|
71
|
+
FOR UPDATE;
|
|
72
|
+
|
|
73
|
+
IF NOT FOUND THEN
|
|
74
|
+
RAISE EXCEPTION 'CRM contact not found';
|
|
75
|
+
END IF;
|
|
76
|
+
|
|
77
|
+
IF v_primary_organization_id IS NULL THEN
|
|
78
|
+
UPDATE public.crm_contacts
|
|
79
|
+
SET organization_id = p_organization_id
|
|
80
|
+
WHERE id = p_contact_id;
|
|
81
|
+
ELSE
|
|
82
|
+
INSERT INTO public.crm_contact_organizations (contact_id, organization_id, is_primary)
|
|
83
|
+
VALUES (p_contact_id, p_organization_id, v_primary_organization_id = p_organization_id)
|
|
84
|
+
ON CONFLICT (contact_id, organization_id) DO NOTHING;
|
|
85
|
+
END IF;
|
|
86
|
+
END;
|
|
87
|
+
$$;
|
|
88
|
+
|
|
89
|
+
REVOKE ALL ON FUNCTION public.link_crm_contact_organization(uuid, uuid)
|
|
90
|
+
FROM PUBLIC, anon, authenticated;
|
|
91
|
+
GRANT EXECUTE ON FUNCTION public.link_crm_contact_organization(uuid, uuid)
|
|
92
|
+
TO service_role;
|
|
93
|
+
|
|
94
|
+
ALTER TABLE public.crm_contact_organizations ENABLE ROW LEVEL SECURITY;
|
|
95
|
+
|
|
96
|
+
DROP POLICY IF EXISTS "Staff manage crm contact organizations" ON public.crm_contact_organizations;
|
|
97
|
+
DROP POLICY IF EXISTS "Staff read crm contact organizations" ON public.crm_contact_organizations;
|
|
98
|
+
CREATE POLICY "Staff read crm contact organizations"
|
|
99
|
+
ON public.crm_contact_organizations
|
|
100
|
+
FOR SELECT
|
|
101
|
+
TO authenticated
|
|
102
|
+
USING (public.is_staff());
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ProjectBoardPage,
|
|
5
|
-
ProjectDetailPage,
|
|
6
|
-
ProjectsPage,
|
|
7
|
-
ProjectTasksPage,
|
|
8
|
-
type ProjectDetailPageProps,
|
|
9
|
-
type ProjectsPageProps,
|
|
10
|
-
type ProjectTasksPageProps,
|
|
11
|
-
} from "@brightweblabs/module-projects/ui";
|
|
12
|
-
|
|
13
|
-
export function ProjectsPageLiveMount(props: ProjectsPageProps) {
|
|
14
|
-
return <ProjectsPage {...props} />;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function ProjectDetailPageLiveMount(props: ProjectDetailPageProps) {
|
|
18
|
-
return <ProjectDetailPage {...props} />;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function ProjectBoardPageLiveMount(props: ProjectTasksPageProps) {
|
|
22
|
-
return <ProjectBoardPage {...props} />;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function ProjectTasksPageLiveMount(props: ProjectTasksPageProps) {
|
|
26
|
-
return <ProjectTasksPage {...props} />;
|
|
27
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { requireServerPageRoleAccess } from "@brightweblabs/core-auth/server";
|
|
2
|
-
import {
|
|
3
|
-
getProjectDashboard,
|
|
4
|
-
getProjectAccess,
|
|
5
|
-
getProjectsPortfolioPageData,
|
|
6
|
-
} from "@brightweblabs/module-projects";
|
|
7
|
-
import {
|
|
8
|
-
ProjectBoardPageLiveMount,
|
|
9
|
-
ProjectDetailPageLiveMount,
|
|
10
|
-
ProjectsPageLiveMount,
|
|
11
|
-
ProjectTasksPageLiveMount,
|
|
12
|
-
} from "./projetos-live-mounts";
|
|
13
|
-
|
|
14
|
-
const navigation = {
|
|
15
|
-
listHref: "/projetos",
|
|
16
|
-
detailHrefPattern: "/projetos/:projectId",
|
|
17
|
-
boardHrefPattern: "/projetos/:projectId/tarefas",
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export async function ProjectsServerMount() {
|
|
21
|
-
await requireServerPageRoleAccess(["admin", "staff"]);
|
|
22
|
-
const { organizationOptions, portfolioStats, result } = await getProjectsPortfolioPageData();
|
|
23
|
-
const attentionSummary = {
|
|
24
|
-
total: result.total,
|
|
25
|
-
overdue: result.items.filter((project) => project.taskStats.overdue > 0).length,
|
|
26
|
-
atRisk: result.items.filter((project) => project.health === "at_risk").length,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<ProjectsPageLiveMount
|
|
31
|
-
initialData={{ ...result, attentionSummary }}
|
|
32
|
-
initialUpdatedAt={new Date().toISOString()}
|
|
33
|
-
portfolioStats={portfolioStats}
|
|
34
|
-
organizations={organizationOptions}
|
|
35
|
-
navigation={navigation}
|
|
36
|
-
/>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function getInitialProject(params: Promise<{ projectId: string }>) {
|
|
41
|
-
const { projectId } = await params;
|
|
42
|
-
const { supabase, profileId, role } = await requireServerPageRoleAccess(["admin", "staff"]);
|
|
43
|
-
const [initialData, access] = await Promise.all([
|
|
44
|
-
getProjectDashboard(supabase, projectId),
|
|
45
|
-
getProjectAccess(supabase, projectId, profileId, role),
|
|
46
|
-
]);
|
|
47
|
-
return { initialData, access };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function ProjectDetailServerMount({ params }: { params: Promise<{ projectId: string }> }) {
|
|
51
|
-
const { initialData, access } = await getInitialProject(params);
|
|
52
|
-
return <ProjectDetailPageLiveMount initialData={initialData} projectRole={access.projectRole} permissions={access.permissions} navigation={navigation} />;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function ProjectBoardServerMount({ params }: { params: Promise<{ projectId: string }> }) {
|
|
56
|
-
const { initialData, access } = await getInitialProject(params);
|
|
57
|
-
return <ProjectBoardPageLiveMount initialData={initialData} projectRole={access.projectRole} permissions={access.permissions} navigation={navigation} />;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export async function ProjectTasksServerMount({ params }: { params: Promise<{ projectId: string }> }) {
|
|
61
|
-
const { initialData, access } = await getInitialProject(params);
|
|
62
|
-
return <ProjectTasksPageLiveMount initialData={initialData} projectRole={access.projectRole} permissions={access.permissions} navigation={navigation} />;
|
|
63
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
handleClientProjectDetailGetRequest,
|
|
3
|
-
handleClientProjectsGetRequest,
|
|
4
|
-
handleProjectClientAccessGetRequest,
|
|
5
|
-
handleProjectClientAccessPatchRequest,
|
|
6
|
-
handleProjectOrganizationsPatchRequest,
|
|
7
|
-
handleProjectsActivityGetRequest,
|
|
8
|
-
handleProjectsAssignableProfilesGetRequest,
|
|
9
|
-
handleProjectsDashboardGetRequest,
|
|
10
|
-
handleProjectsDeleteRequest,
|
|
11
|
-
handleProjectsGetRequest,
|
|
12
|
-
handleProjectsLinksDeleteRequest,
|
|
13
|
-
handleProjectsLinksPatchRequest,
|
|
14
|
-
handleProjectsLinksPostRequest,
|
|
15
|
-
handleProjectsMembersPutRequest,
|
|
16
|
-
handleProjectsMilestonesDeleteRequest,
|
|
17
|
-
handleProjectsMilestonesPatchRequest,
|
|
18
|
-
handleProjectsMilestonesPostRequest,
|
|
19
|
-
handleProjectsOrganizationsGetRequest,
|
|
20
|
-
handleProjectsOrganizationsPostRequest,
|
|
21
|
-
handleProjectsPatchRequest,
|
|
22
|
-
handleProjectsPostRequest,
|
|
23
|
-
handleProjectsStatsGetRequest,
|
|
24
|
-
handleProjectsSetupOptionsGetRequest,
|
|
25
|
-
handleProjectsTasksDeleteRequest,
|
|
26
|
-
handleProjectsTasksPatchRequest,
|
|
27
|
-
handleProjectsTasksPostRequest,
|
|
28
|
-
} from "@brightweblabs/module-projects";
|
|
29
|
-
|
|
30
|
-
type IdRouteContext = { params: Promise<{ id: string }> };
|
|
31
|
-
type ItemRouteContext = { params: Promise<{ id: string; itemId: string }> };
|
|
32
|
-
|
|
33
|
-
export function projectsGet(request: Request) {
|
|
34
|
-
return handleProjectsGetRequest(request);
|
|
35
|
-
}
|
|
36
|
-
export function clientProjectsGet(request: Request) {
|
|
37
|
-
return handleClientProjectsGetRequest(request);
|
|
38
|
-
}
|
|
39
|
-
export function clientProjectDetailGet(request: Request, context: IdRouteContext) {
|
|
40
|
-
return handleClientProjectDetailGetRequest(request, context);
|
|
41
|
-
}
|
|
42
|
-
export function projectClientAccessGet(request: Request, context: IdRouteContext) {
|
|
43
|
-
return handleProjectClientAccessGetRequest(request, context);
|
|
44
|
-
}
|
|
45
|
-
export function projectClientAccessPatch(request: Request, context: IdRouteContext) {
|
|
46
|
-
return handleProjectClientAccessPatchRequest(request, context);
|
|
47
|
-
}
|
|
48
|
-
export function projectOrganizationsPatch(request: Request, context: IdRouteContext) {
|
|
49
|
-
return handleProjectOrganizationsPatchRequest(request, context);
|
|
50
|
-
}
|
|
51
|
-
export function projectsSetupOptionsGet(request: Request) {
|
|
52
|
-
return handleProjectsSetupOptionsGetRequest(request);
|
|
53
|
-
}
|
|
54
|
-
export function projectsPost(request: Request) {
|
|
55
|
-
return handleProjectsPostRequest(request);
|
|
56
|
-
}
|
|
57
|
-
export function projectsStatsGet(request: Request) {
|
|
58
|
-
return handleProjectsStatsGetRequest(request);
|
|
59
|
-
}
|
|
60
|
-
export function projectsOrganizationsGet(request: Request) {
|
|
61
|
-
return handleProjectsOrganizationsGetRequest(request);
|
|
62
|
-
}
|
|
63
|
-
export function projectsOrganizationsPost(request: Request) {
|
|
64
|
-
return handleProjectsOrganizationsPostRequest(request);
|
|
65
|
-
}
|
|
66
|
-
export function projectDashboardGet(request: Request, context: IdRouteContext) {
|
|
67
|
-
return handleProjectsDashboardGetRequest(request, context);
|
|
68
|
-
}
|
|
69
|
-
export function projectPatch(request: Request, context: IdRouteContext) {
|
|
70
|
-
return handleProjectsPatchRequest(request, context);
|
|
71
|
-
}
|
|
72
|
-
export function projectDelete(request: Request, context: IdRouteContext) {
|
|
73
|
-
return handleProjectsDeleteRequest(request, context);
|
|
74
|
-
}
|
|
75
|
-
export function projectActivityGet(request: Request, context: IdRouteContext) {
|
|
76
|
-
return handleProjectsActivityGetRequest(request, context);
|
|
77
|
-
}
|
|
78
|
-
export function projectMembersGet(request: Request, context: IdRouteContext) {
|
|
79
|
-
return handleProjectsAssignableProfilesGetRequest(request, context);
|
|
80
|
-
}
|
|
81
|
-
export function projectMembersPut(request: Request, context: IdRouteContext) {
|
|
82
|
-
return handleProjectsMembersPutRequest(request, context);
|
|
83
|
-
}
|
|
84
|
-
export function projectLinksPost(request: Request, context: IdRouteContext) {
|
|
85
|
-
return handleProjectsLinksPostRequest(request, context);
|
|
86
|
-
}
|
|
87
|
-
export function projectLinkPatch(request: Request, context: ItemRouteContext) {
|
|
88
|
-
return handleProjectsLinksPatchRequest(request, context);
|
|
89
|
-
}
|
|
90
|
-
export function projectLinkDelete(request: Request, context: ItemRouteContext) {
|
|
91
|
-
return handleProjectsLinksDeleteRequest(request, context);
|
|
92
|
-
}
|
|
93
|
-
export function projectMilestonesPost(request: Request, context: IdRouteContext) {
|
|
94
|
-
return handleProjectsMilestonesPostRequest(request, context);
|
|
95
|
-
}
|
|
96
|
-
export function projectMilestonePatch(request: Request, context: ItemRouteContext) {
|
|
97
|
-
return handleProjectsMilestonesPatchRequest(request, context);
|
|
98
|
-
}
|
|
99
|
-
export function projectMilestoneDelete(request: Request, context: ItemRouteContext) {
|
|
100
|
-
return handleProjectsMilestonesDeleteRequest(request, context);
|
|
101
|
-
}
|
|
102
|
-
export function projectTasksPost(request: Request, context: IdRouteContext) {
|
|
103
|
-
return handleProjectsTasksPostRequest(request, context);
|
|
104
|
-
}
|
|
105
|
-
export function projectTaskPatch(request: Request, context: ItemRouteContext) {
|
|
106
|
-
return handleProjectsTasksPatchRequest(request, context);
|
|
107
|
-
}
|
|
108
|
-
export function projectTaskDelete(request: Request, context: ItemRouteContext) {
|
|
109
|
-
return handleProjectsTasksDeleteRequest(request, context);
|
|
110
|
-
}
|