@flytedesk/app-kit 0.1.2 → 0.2.0
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/cli/profile-sync.d.ts +2 -0
- package/dist/cli/profile-sync.js +26 -0
- package/dist/cli/profile-sync.js.map +1 -0
- package/dist/cli/sync-engine.d.ts +20 -0
- package/dist/cli/sync-engine.js +392 -0
- package/dist/cli/sync-engine.js.map +1 -0
- package/dist/cli/trace-sync.js +9 -345
- package/dist/cli/trace-sync.js.map +1 -1
- package/dist/profile/index.d.ts +36 -0
- package/dist/profile/index.js +36 -0
- package/dist/profile/index.js.map +1 -0
- package/dist/profile/plugin.d.ts +3 -0
- package/dist/profile/plugin.js +111 -0
- package/dist/profile/plugin.js.map +1 -0
- package/dist/profile/types.d.ts +76 -0
- package/dist/profile/types.js +10 -0
- package/dist/profile/types.js.map +1 -0
- package/package.json +7 -2
- package/prisma/fragments/profile.meta.json +4 -0
- package/prisma/fragments/profile.prisma +42 -0
- package/prisma/migrations/0002_app_kit_profile_init/migration.sql +15 -0
- package/prisma/migrations/manifest.json +8 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* profile-sync — the CLI half of @flytedesk/app-kit/profile (DEC-39, DEC-52).
|
|
4
|
+
*
|
|
5
|
+
* A thin wrapper around the generalized sync engine in ./sync-engine.js — see that
|
|
6
|
+
* file's doc comment for the full subcommand reference (init/diff/apply/status) and
|
|
7
|
+
* how fragments/migrations/lock files work. This file only supplies the "profile"
|
|
8
|
+
* fragment name and the "profile-sync" bin name; trace-sync.ts is the same wrapper
|
|
9
|
+
* for the trace fragment.
|
|
10
|
+
*/
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { runSyncCli, SyncCliError } from "./sync-engine.js";
|
|
13
|
+
export async function main(argv) {
|
|
14
|
+
return runSyncCli(argv, { name: "profile", binName: "profile-sync" });
|
|
15
|
+
}
|
|
16
|
+
const isMainModule = process.argv[1] !== undefined &&
|
|
17
|
+
import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
18
|
+
if (isMainModule) {
|
|
19
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
20
|
+
if (!(err instanceof SyncCliError)) {
|
|
21
|
+
console.error(err);
|
|
22
|
+
}
|
|
23
|
+
process.exitCode = process.exitCode ?? 1;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=profile-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile-sync.js","sourceRoot":"","sources":["../../src/cli/profile-sync.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAE5D,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAc;IACvC,OAAO,UAAU,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,YAAY,GAChB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS;IAC7B,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAE1D,IAAI,YAAY,EAAE,CAAC;IACjB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACjD,IAAI,CAAC,CAAC,GAAG,YAAY,YAAY,CAAC,EAAE,CAAC;YACnC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;QACD,OAAO,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything that differs between `trace-sync` and `profile-sync`. `name` picks out
|
|
3
|
+
* prisma/fragments/<name>.prisma, prisma/fragments/<name>.meta.json, and every
|
|
4
|
+
* manifest entry with `fragment: name`; `binName` is only used for log/error message
|
|
5
|
+
* prefixes and the CLI's own usage string.
|
|
6
|
+
*/
|
|
7
|
+
export interface SyncFragment {
|
|
8
|
+
/** e.g. "trace" or "profile" — must match the fragment's file basenames and its
|
|
9
|
+
* manifest entries' `fragment` field. */
|
|
10
|
+
name: string;
|
|
11
|
+
/** e.g. "trace-sync" or "profile-sync" — the bin name this is invoked as. */
|
|
12
|
+
binName: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class SyncCliError extends Error {
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Runs one CLI invocation for the given fragment. Called by each bin wrapper
|
|
18
|
+
* (src/cli/trace-sync.ts, src/cli/profile-sync.ts) with `process.argv.slice(2)`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function runSyncCli(argv: string[], fragment: SyncFragment): Promise<void>;
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sync-engine — the shared internals behind `trace-sync` and `profile-sync`
|
|
3
|
+
* (DEC-39, generalized to a second fragment for the profile module).
|
|
4
|
+
*
|
|
5
|
+
* This package never generates its own Prisma Client for any of its fragments.
|
|
6
|
+
* Instead, this engine copies a package fragment (prisma/fragments/<fragment>.prisma)
|
|
7
|
+
* into the *consumer's* own multi-file Prisma schema folder, applies that fragment's
|
|
8
|
+
* raw SQL migrations directly against the consumer's configured datasource, and
|
|
9
|
+
* tracks what's been applied in a lock file (`.app-kit-<fragment>.lock.json`) written
|
|
10
|
+
* at the consumer project root. The consumer's own `prisma generate` then produces
|
|
11
|
+
* one client that includes these models alongside their own.
|
|
12
|
+
*
|
|
13
|
+
* Both bin commands are thin wrappers around `runSyncCli` (see src/cli/trace-sync.ts
|
|
14
|
+
* and src/cli/profile-sync.ts) — everything fragment-specific is passed in as a
|
|
15
|
+
* `SyncFragment` rather than hardcoded, so a third fragment only ever needs its own
|
|
16
|
+
* ~15-line wrapper file, not a second copy of this logic.
|
|
17
|
+
*
|
|
18
|
+
* Migrations for every fragment live together under prisma/migrations/ and are
|
|
19
|
+
* tracked in one shared prisma/migrations/manifest.json, disambiguated by each
|
|
20
|
+
* entry's `fragment` field — `computePending`/`cmdInit`/`cmdApply` below all filter
|
|
21
|
+
* the manifest down to `entry.fragment === fragment.name` before doing anything, so
|
|
22
|
+
* e.g. `trace-sync init` only ever touches trace's own migrations, never profile's.
|
|
23
|
+
*
|
|
24
|
+
* Subcommands (identical across every fragment):
|
|
25
|
+
* <bin> init copy fragment + migrations in, run `prisma validate` and
|
|
26
|
+
* `prisma db execute`, write the lock file.
|
|
27
|
+
* <bin> diff read-only: compare this package's bundled fragment version /
|
|
28
|
+
* migration manifest against the consumer's lock file and report
|
|
29
|
+
* what's pending. Never applies anything.
|
|
30
|
+
* <bin> apply apply pending migrations + refresh the fragment copy, update the
|
|
31
|
+
* lock file. Does not re-run `prisma validate` against a fresh
|
|
32
|
+
* init — use `init` for first install.
|
|
33
|
+
* <bin> status read-only report: fragment version, applied migrations, and
|
|
34
|
+
* whether the consumer's copy of the fragment has been hand-edited
|
|
35
|
+
* (checksum mismatch).
|
|
36
|
+
*
|
|
37
|
+
* This is a real, runnable CLI — it shells out to the consumer's local `prisma` via
|
|
38
|
+
* `npx` and touches the consumer's real filesystem — not a stub.
|
|
39
|
+
*/
|
|
40
|
+
import { createHash } from "node:crypto";
|
|
41
|
+
import spawn from "cross-spawn";
|
|
42
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
43
|
+
import path from "node:path";
|
|
44
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
45
|
+
export class SyncCliError extends Error {
|
|
46
|
+
}
|
|
47
|
+
function readJson(filePath) {
|
|
48
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
49
|
+
}
|
|
50
|
+
function sha256(content) {
|
|
51
|
+
return `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
52
|
+
}
|
|
53
|
+
function sha256File(filePath) {
|
|
54
|
+
return sha256(readFileSync(filePath));
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Engine: bound to one SyncFragment + the consumer root it's operating on
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
class SyncEngine {
|
|
60
|
+
fragment;
|
|
61
|
+
packageRoot;
|
|
62
|
+
fragmentSrcPath;
|
|
63
|
+
fragmentMetaPath;
|
|
64
|
+
migrationsSrcDir;
|
|
65
|
+
manifestSrcPath;
|
|
66
|
+
lockFileName;
|
|
67
|
+
constructor(fragment) {
|
|
68
|
+
this.fragment = fragment;
|
|
69
|
+
// Root of the installed @flytedesk/app-kit package (two levels up from
|
|
70
|
+
// dist/cli/<bin>.js — this file, once built).
|
|
71
|
+
this.packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
72
|
+
this.fragmentSrcPath = path.join(this.packageRoot, "prisma", "fragments", `${fragment.name}.prisma`);
|
|
73
|
+
this.fragmentMetaPath = path.join(this.packageRoot, "prisma", "fragments", `${fragment.name}.meta.json`);
|
|
74
|
+
this.migrationsSrcDir = path.join(this.packageRoot, "prisma", "migrations");
|
|
75
|
+
this.manifestSrcPath = path.join(this.migrationsSrcDir, "manifest.json");
|
|
76
|
+
this.lockFileName = `.app-kit-${fragment.name}.lock.json`;
|
|
77
|
+
}
|
|
78
|
+
fail(message) {
|
|
79
|
+
console.error(`${this.fragment.binName}: ${message}`);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
throw new SyncCliError(message);
|
|
82
|
+
}
|
|
83
|
+
readFragmentMeta() {
|
|
84
|
+
return readJson(this.fragmentMetaPath);
|
|
85
|
+
}
|
|
86
|
+
/** Every manifest entry for THIS fragment only — see the module doc comment on
|
|
87
|
+
* why the manifest is shared across fragments and how entries are disambiguated. */
|
|
88
|
+
readManifest() {
|
|
89
|
+
return readJson(this.manifestSrcPath).filter((entry) => entry.fragment === this.fragment.name);
|
|
90
|
+
}
|
|
91
|
+
lockFilePath(consumerRoot) {
|
|
92
|
+
return path.join(consumerRoot, this.lockFileName);
|
|
93
|
+
}
|
|
94
|
+
readLockFile(consumerRoot) {
|
|
95
|
+
const p = this.lockFilePath(consumerRoot);
|
|
96
|
+
if (!existsSync(p))
|
|
97
|
+
return null;
|
|
98
|
+
return readJson(p);
|
|
99
|
+
}
|
|
100
|
+
writeLockFile(consumerRoot, lock) {
|
|
101
|
+
writeFileSync(this.lockFilePath(consumerRoot), `${JSON.stringify(lock, null, 2)}\n`, "utf8");
|
|
102
|
+
}
|
|
103
|
+
// -------------------------------------------------------------------------
|
|
104
|
+
// Locating the consumer's Prisma schema folder via their prisma.config.ts
|
|
105
|
+
// -------------------------------------------------------------------------
|
|
106
|
+
static CONSUMER_CONFIG_CANDIDATES = [
|
|
107
|
+
"prisma.config.ts",
|
|
108
|
+
"prisma.config.mts",
|
|
109
|
+
"prisma.config.js",
|
|
110
|
+
"prisma.config.mjs",
|
|
111
|
+
];
|
|
112
|
+
findConsumerConfigFile(consumerRoot) {
|
|
113
|
+
for (const candidate of SyncEngine.CONSUMER_CONFIG_CANDIDATES) {
|
|
114
|
+
const p = path.join(consumerRoot, candidate);
|
|
115
|
+
if (existsSync(p))
|
|
116
|
+
return p;
|
|
117
|
+
}
|
|
118
|
+
this.fail(`could not find a prisma.config.{ts,mts,js,mjs} in ${consumerRoot}. ` +
|
|
119
|
+
`${this.fragment.binName} reads this file to find your Prisma schema folder — it does not assume a path.`);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Best-effort read of the consumer's `schema` setting out of their
|
|
123
|
+
* prisma.config.ts default export. Supports the shapes Prisma's config API has
|
|
124
|
+
* used: a bare string path (file or folder), or `{ path: string }`. Relies on
|
|
125
|
+
* Node's native TypeScript type-stripping (unflagged as of the Node 24 baseline
|
|
126
|
+
* this package requires) to import a .ts file directly — if the consumer's config
|
|
127
|
+
* uses syntax that isn't erasable (enums, namespaces, parameter properties, etc.)
|
|
128
|
+
* this import will fail, and that failure is surfaced as-is rather than swallowed.
|
|
129
|
+
*/
|
|
130
|
+
async loadConsumerSchemaPath(consumerRoot) {
|
|
131
|
+
const configFile = this.findConsumerConfigFile(consumerRoot);
|
|
132
|
+
let mod;
|
|
133
|
+
try {
|
|
134
|
+
mod = await import(pathToFileURL(configFile).href);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
this.fail(`failed to load ${configFile}: ${err.message}\n` +
|
|
138
|
+
`If this config file uses TypeScript syntax that Node can't type-strip natively ` +
|
|
139
|
+
`(enums, namespaces, decorators, ...), ${this.fragment.binName} can't import it directly.`);
|
|
140
|
+
}
|
|
141
|
+
const config = mod.default ?? mod;
|
|
142
|
+
const schema = config?.schema;
|
|
143
|
+
let schemaPath;
|
|
144
|
+
if (typeof schema === "string") {
|
|
145
|
+
schemaPath = schema;
|
|
146
|
+
}
|
|
147
|
+
else if (schema &&
|
|
148
|
+
typeof schema === "object" &&
|
|
149
|
+
typeof schema.path === "string") {
|
|
150
|
+
schemaPath = schema.path;
|
|
151
|
+
}
|
|
152
|
+
if (!schemaPath) {
|
|
153
|
+
this.fail(`could not find a "schema" path in ${configFile}'s default export. ` +
|
|
154
|
+
`Expected \`defineConfig({ schema: "prisma/schema" })\` (or a { path } object).`);
|
|
155
|
+
}
|
|
156
|
+
return path.resolve(path.dirname(configFile), schemaPath);
|
|
157
|
+
}
|
|
158
|
+
/** Resolve the folder <fragment>.prisma gets copied into, from the config's
|
|
159
|
+
* schema path (which may point at the folder itself, or at a single schema file
|
|
160
|
+
* inside it, e.g. "prisma/schema.prisma"). */
|
|
161
|
+
resolveSchemaFolder(schemaPath) {
|
|
162
|
+
if (schemaPath.endsWith(".prisma")) {
|
|
163
|
+
return path.dirname(schemaPath);
|
|
164
|
+
}
|
|
165
|
+
return schemaPath;
|
|
166
|
+
}
|
|
167
|
+
/** The conventional "prisma root" a schema folder sits under, used to locate (or
|
|
168
|
+
* create) the sibling `migrations/` directory. For the common multi-file layout
|
|
169
|
+
* `prisma/schema/*.prisma`, that's `prisma/`; for a flat `prisma/schema.prisma`
|
|
170
|
+
* layout, `resolveSchemaFolder` already returned `prisma/`, so this is a no-op. */
|
|
171
|
+
resolvePrismaRoot(schemaFolder) {
|
|
172
|
+
return path.basename(schemaFolder) === "schema"
|
|
173
|
+
? path.dirname(schemaFolder)
|
|
174
|
+
: schemaFolder;
|
|
175
|
+
}
|
|
176
|
+
// -------------------------------------------------------------------------
|
|
177
|
+
// Shelling out to the consumer's local `prisma`
|
|
178
|
+
// -------------------------------------------------------------------------
|
|
179
|
+
runPrisma(args, cwd) {
|
|
180
|
+
// On Windows, npx resolves to npx.cmd, a shell script — node:child_process's
|
|
181
|
+
// spawnSync can't exec it directly (EINVAL) without shell:true, and shell:true
|
|
182
|
+
// with an argument array is an unescaped-concatenation footgun (path arguments
|
|
183
|
+
// here can contain spaces). cross-spawn resolves and correctly quotes .cmd/.bat
|
|
184
|
+
// executables on Windows without going through a shell at all.
|
|
185
|
+
const result = spawn.sync("npx", ["prisma", ...args], { cwd, stdio: "inherit" });
|
|
186
|
+
if (result.error) {
|
|
187
|
+
this.fail(`failed to run "npx prisma ${args.join(" ")}": ${result.error.message}`);
|
|
188
|
+
}
|
|
189
|
+
if (result.status !== 0) {
|
|
190
|
+
this.fail(`"npx prisma ${args.join(" ")}" exited with code ${result.status}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// -------------------------------------------------------------------------
|
|
194
|
+
// File copying
|
|
195
|
+
// -------------------------------------------------------------------------
|
|
196
|
+
copyFragment(schemaFolder) {
|
|
197
|
+
mkdirSync(schemaFolder, { recursive: true });
|
|
198
|
+
copyFileSync(this.fragmentSrcPath, path.join(schemaFolder, `${this.fragment.name}.prisma`));
|
|
199
|
+
}
|
|
200
|
+
copyMigration(migrationId, migrationsDestDir) {
|
|
201
|
+
const srcDir = path.join(this.migrationsSrcDir, migrationId);
|
|
202
|
+
const destDir = path.join(migrationsDestDir, migrationId);
|
|
203
|
+
mkdirSync(destDir, { recursive: true });
|
|
204
|
+
cpSync(srcDir, destDir, { recursive: true });
|
|
205
|
+
}
|
|
206
|
+
// -------------------------------------------------------------------------
|
|
207
|
+
// Subcommands
|
|
208
|
+
// -------------------------------------------------------------------------
|
|
209
|
+
async cmdInit(consumerRoot) {
|
|
210
|
+
const bin = this.fragment.binName;
|
|
211
|
+
const schemaPath = await this.loadConsumerSchemaPath(consumerRoot);
|
|
212
|
+
const schemaFolder = this.resolveSchemaFolder(schemaPath);
|
|
213
|
+
const prismaRoot = this.resolvePrismaRoot(schemaFolder);
|
|
214
|
+
const migrationsDestDir = path.join(prismaRoot, "migrations");
|
|
215
|
+
console.log(`${bin}: copying ${this.fragment.name}.prisma into ${schemaFolder}`);
|
|
216
|
+
this.copyFragment(schemaFolder);
|
|
217
|
+
console.log(`${bin}: running "npx prisma validate" against ${consumerRoot}`);
|
|
218
|
+
this.runPrisma(["validate"], consumerRoot);
|
|
219
|
+
const manifest = this.readManifest();
|
|
220
|
+
const appliedMigrations = [];
|
|
221
|
+
for (const entry of manifest) {
|
|
222
|
+
console.log(`${bin}: copying migration ${entry.id} into ${migrationsDestDir}`);
|
|
223
|
+
this.copyMigration(entry.id, migrationsDestDir);
|
|
224
|
+
const migrationSqlPath = path.join(migrationsDestDir, entry.id, "migration.sql");
|
|
225
|
+
console.log(`${bin}: applying ${entry.id} via "npx prisma db execute"`);
|
|
226
|
+
this.runPrisma(
|
|
227
|
+
// Prisma ORM v7's `db execute` no longer accepts `--schema` at all — the
|
|
228
|
+
// datasource URL comes only from the consumer's prisma.config.ts now (same
|
|
229
|
+
// "config file, not CLI flags" migration documented on flytedesk-id's own
|
|
230
|
+
// prisma.config.ts). Passing --schema here is a hard CLI usage error
|
|
231
|
+
// ("unknown or unexpected option: --schema") against a real Prisma 7 install,
|
|
232
|
+
// confirmed while building examples/hello-world.
|
|
233
|
+
["db", "execute", "--file", migrationSqlPath], consumerRoot);
|
|
234
|
+
appliedMigrations.push(entry.id);
|
|
235
|
+
}
|
|
236
|
+
const meta = this.readFragmentMeta();
|
|
237
|
+
this.writeLockFile(consumerRoot, {
|
|
238
|
+
fragmentVersion: meta.fragmentVersion,
|
|
239
|
+
fragmentChecksum: sha256File(this.fragmentSrcPath),
|
|
240
|
+
appliedMigrations,
|
|
241
|
+
lastSyncedAt: new Date().toISOString(),
|
|
242
|
+
});
|
|
243
|
+
console.log(`${bin}: wrote ${this.lockFilePath(consumerRoot)}`);
|
|
244
|
+
console.log(`${bin}: init complete.`);
|
|
245
|
+
}
|
|
246
|
+
computePending(consumerRoot) {
|
|
247
|
+
const lock = this.readLockFile(consumerRoot);
|
|
248
|
+
if (!lock) {
|
|
249
|
+
this.fail(`no ${this.lockFileName} found at ${consumerRoot}. Run "${this.fragment.binName} init" first.`);
|
|
250
|
+
}
|
|
251
|
+
const meta = this.readFragmentMeta();
|
|
252
|
+
const manifest = this.readManifest();
|
|
253
|
+
const applied = new Set(lock.appliedMigrations);
|
|
254
|
+
const pendingMigrations = manifest.filter((entry) => !applied.has(entry.id));
|
|
255
|
+
return {
|
|
256
|
+
fragmentVersionCurrent: meta.fragmentVersion,
|
|
257
|
+
fragmentVersionInstalled: lock.fragmentVersion,
|
|
258
|
+
fragmentVersionChanged: meta.fragmentVersion !== lock.fragmentVersion,
|
|
259
|
+
pendingMigrations,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
cmdDiff(consumerRoot) {
|
|
263
|
+
const report = this.computePending(consumerRoot);
|
|
264
|
+
if (report.fragmentVersionChanged) {
|
|
265
|
+
console.log(`fragment version: ${report.fragmentVersionInstalled} -> ${report.fragmentVersionCurrent} (changed)`);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
console.log(`fragment version: ${report.fragmentVersionCurrent} (up to date)`);
|
|
269
|
+
}
|
|
270
|
+
if (report.pendingMigrations.length === 0) {
|
|
271
|
+
console.log("migrations: up to date, nothing pending.");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
console.log(`migrations: ${report.pendingMigrations.length} pending:`);
|
|
275
|
+
for (const entry of report.pendingMigrations) {
|
|
276
|
+
console.log(` - ${entry.id} (introduced in fragment ${entry.introducedInFragmentVersion})`);
|
|
277
|
+
}
|
|
278
|
+
console.log(`${this.fragment.binName} diff is read-only. Run "${this.fragment.binName} apply" to apply these.`);
|
|
279
|
+
}
|
|
280
|
+
async cmdApply(consumerRoot) {
|
|
281
|
+
const bin = this.fragment.binName;
|
|
282
|
+
const report = this.computePending(consumerRoot);
|
|
283
|
+
const schemaPath = await this.loadConsumerSchemaPath(consumerRoot);
|
|
284
|
+
const schemaFolder = this.resolveSchemaFolder(schemaPath);
|
|
285
|
+
const prismaRoot = this.resolvePrismaRoot(schemaFolder);
|
|
286
|
+
const migrationsDestDir = path.join(prismaRoot, "migrations");
|
|
287
|
+
console.log(`${bin}: refreshing ${this.fragment.name}.prisma in ${schemaFolder}`);
|
|
288
|
+
this.copyFragment(schemaFolder);
|
|
289
|
+
const lock = this.readLockFile(consumerRoot);
|
|
290
|
+
if (!lock) {
|
|
291
|
+
this.fail(`no ${this.lockFileName} found at ${consumerRoot}. Run "${bin} init" first.`);
|
|
292
|
+
}
|
|
293
|
+
const appliedMigrations = [...lock.appliedMigrations];
|
|
294
|
+
if (report.pendingMigrations.length === 0) {
|
|
295
|
+
console.log(`${bin}: no pending migrations to apply.`);
|
|
296
|
+
}
|
|
297
|
+
for (const entry of report.pendingMigrations) {
|
|
298
|
+
console.log(`${bin}: copying migration ${entry.id} into ${migrationsDestDir}`);
|
|
299
|
+
this.copyMigration(entry.id, migrationsDestDir);
|
|
300
|
+
const migrationSqlPath = path.join(migrationsDestDir, entry.id, "migration.sql");
|
|
301
|
+
console.log(`${bin}: applying ${entry.id} via "npx prisma db execute"`);
|
|
302
|
+
this.runPrisma(
|
|
303
|
+
// See the matching comment in cmdInit above — Prisma 7's `db execute` has no
|
|
304
|
+
// --schema flag; the datasource comes from the consumer's prisma.config.ts.
|
|
305
|
+
["db", "execute", "--file", migrationSqlPath], consumerRoot);
|
|
306
|
+
appliedMigrations.push(entry.id);
|
|
307
|
+
}
|
|
308
|
+
const meta = this.readFragmentMeta();
|
|
309
|
+
this.writeLockFile(consumerRoot, {
|
|
310
|
+
fragmentVersion: meta.fragmentVersion,
|
|
311
|
+
fragmentChecksum: sha256File(this.fragmentSrcPath),
|
|
312
|
+
appliedMigrations,
|
|
313
|
+
lastSyncedAt: new Date().toISOString(),
|
|
314
|
+
});
|
|
315
|
+
console.log(`${bin}: wrote ${this.lockFilePath(consumerRoot)}`);
|
|
316
|
+
console.log(`${bin}: apply complete.`);
|
|
317
|
+
}
|
|
318
|
+
async cmdStatus(consumerRoot) {
|
|
319
|
+
const lock = this.readLockFile(consumerRoot);
|
|
320
|
+
if (!lock) {
|
|
321
|
+
console.log(`no ${this.lockFileName} found at ${consumerRoot}. Not yet initialized — run "${this.fragment.binName} init".`);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
console.log(`fragment version (installed): ${lock.fragmentVersion}`);
|
|
325
|
+
console.log(`fragment version (package): ${this.readFragmentMeta().fragmentVersion}`);
|
|
326
|
+
console.log(`last synced: ${lock.lastSyncedAt}`);
|
|
327
|
+
console.log(`applied migrations (${lock.appliedMigrations.length}):`);
|
|
328
|
+
for (const id of lock.appliedMigrations) {
|
|
329
|
+
console.log(` - ${id}`);
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
const schemaPath = await this.loadConsumerSchemaPath(consumerRoot);
|
|
333
|
+
const schemaFolder = this.resolveSchemaFolder(schemaPath);
|
|
334
|
+
const copiedFragmentPath = path.join(schemaFolder, `${this.fragment.name}.prisma`);
|
|
335
|
+
if (!existsSync(copiedFragmentPath)) {
|
|
336
|
+
console.log(`fragment copy: MISSING at ${copiedFragmentPath}`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const installedChecksum = sha256File(copiedFragmentPath);
|
|
340
|
+
if (installedChecksum === lock.fragmentChecksum) {
|
|
341
|
+
console.log("fragment copy: unmodified");
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
console.log(`fragment copy: CHECKSUM MISMATCH — ${copiedFragmentPath} has been hand-edited ` +
|
|
345
|
+
`(or is out of sync with the lock file)`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
console.log(`fragment copy: could not verify (${err.message})`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// Entry point shared by every fragment's bin wrapper
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
const SUBCOMMANDS = ["init", "diff", "apply", "status"];
|
|
357
|
+
function isSubcommand(value) {
|
|
358
|
+
return SUBCOMMANDS.includes(value);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Runs one CLI invocation for the given fragment. Called by each bin wrapper
|
|
362
|
+
* (src/cli/trace-sync.ts, src/cli/profile-sync.ts) with `process.argv.slice(2)`.
|
|
363
|
+
*/
|
|
364
|
+
export async function runSyncCli(argv, fragment) {
|
|
365
|
+
const [subcommand, ...rest] = argv;
|
|
366
|
+
if (!isSubcommand(subcommand)) {
|
|
367
|
+
console.error(`usage: ${fragment.binName} <${SUBCOMMANDS.join("|")}>`);
|
|
368
|
+
process.exitCode = 1;
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
// Reserved for a future --cwd override; today every sync CLI always operates on
|
|
372
|
+
// the directory it's invoked from, which is how `npx <bin> ...` / an npm script in
|
|
373
|
+
// the consumer's own package.json will run it.
|
|
374
|
+
void rest;
|
|
375
|
+
const consumerRoot = process.cwd();
|
|
376
|
+
const engine = new SyncEngine(fragment);
|
|
377
|
+
switch (subcommand) {
|
|
378
|
+
case "init":
|
|
379
|
+
await engine.cmdInit(consumerRoot);
|
|
380
|
+
break;
|
|
381
|
+
case "diff":
|
|
382
|
+
engine.cmdDiff(consumerRoot);
|
|
383
|
+
break;
|
|
384
|
+
case "apply":
|
|
385
|
+
await engine.cmdApply(consumerRoot);
|
|
386
|
+
break;
|
|
387
|
+
case "status":
|
|
388
|
+
await engine.cmdStatus(consumerRoot);
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
//# sourceMappingURL=sync-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-engine.js","sourceRoot":"","sources":["../../src/cli/sync-engine.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/trace-sync.js
CHANGED
|
@@ -2,358 +2,22 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* trace-sync — the CLI half of @flytedesk/app-kit/trace (DEC-39).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* consumer's own `prisma generate` then produces one client that includes these
|
|
11
|
-
* models alongside their own.
|
|
12
|
-
*
|
|
13
|
-
* Subcommands:
|
|
14
|
-
* trace-sync init copy fragment + migration in, run `prisma validate` and
|
|
15
|
-
* `prisma db execute`, write the lock file.
|
|
16
|
-
* trace-sync diff read-only: compare this package's bundled fragment
|
|
17
|
-
* version / migration manifest against the consumer's lock
|
|
18
|
-
* file and report what's pending. Never applies anything.
|
|
19
|
-
* trace-sync apply apply pending migrations + refresh the fragment copy,
|
|
20
|
-
* update the lock file. Does not re-run `prisma validate`
|
|
21
|
-
* against a fresh init — use `init` for first install.
|
|
22
|
-
* trace-sync status read-only report: fragment version, applied migrations,
|
|
23
|
-
* and whether the consumer's copy of the fragment has been
|
|
24
|
-
* hand-edited (checksum mismatch).
|
|
25
|
-
*
|
|
26
|
-
* This is a real, runnable CLI — it shells out to the consumer's local `prisma`
|
|
27
|
-
* via `npx` and touches the consumer's real filesystem — not a stub.
|
|
28
|
-
*/
|
|
29
|
-
import { createHash } from "node:crypto";
|
|
30
|
-
import spawn from "cross-spawn";
|
|
31
|
-
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
32
|
-
import path from "node:path";
|
|
33
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// Package-relative paths
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
/** Root of the installed @flytedesk/app-kit package (two levels up from
|
|
38
|
-
* dist/cli/trace-sync.js — this file, once built). */
|
|
39
|
-
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
40
|
-
const fragmentSrcPath = path.join(packageRoot, "prisma", "fragments", "trace.prisma");
|
|
41
|
-
const fragmentMetaPath = path.join(packageRoot, "prisma", "fragments", "trace.meta.json");
|
|
42
|
-
const migrationsSrcDir = path.join(packageRoot, "prisma", "migrations");
|
|
43
|
-
const manifestSrcPath = path.join(migrationsSrcDir, "manifest.json");
|
|
44
|
-
const LOCK_FILE_NAME = ".app-kit-trace.lock.json";
|
|
45
|
-
class TraceSyncError extends Error {
|
|
46
|
-
}
|
|
47
|
-
function fail(message) {
|
|
48
|
-
console.error(`trace-sync: ${message}`);
|
|
49
|
-
process.exitCode = 1;
|
|
50
|
-
throw new TraceSyncError(message);
|
|
51
|
-
}
|
|
52
|
-
function readJson(filePath) {
|
|
53
|
-
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
54
|
-
}
|
|
55
|
-
function sha256(content) {
|
|
56
|
-
return `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
57
|
-
}
|
|
58
|
-
function sha256File(filePath) {
|
|
59
|
-
return sha256(readFileSync(filePath));
|
|
60
|
-
}
|
|
61
|
-
function readFragmentMeta() {
|
|
62
|
-
return readJson(fragmentMetaPath);
|
|
63
|
-
}
|
|
64
|
-
function readManifest() {
|
|
65
|
-
return readJson(manifestSrcPath);
|
|
66
|
-
}
|
|
67
|
-
function lockFilePath(consumerRoot) {
|
|
68
|
-
return path.join(consumerRoot, LOCK_FILE_NAME);
|
|
69
|
-
}
|
|
70
|
-
function readLockFile(consumerRoot) {
|
|
71
|
-
const p = lockFilePath(consumerRoot);
|
|
72
|
-
if (!existsSync(p))
|
|
73
|
-
return null;
|
|
74
|
-
return readJson(p);
|
|
75
|
-
}
|
|
76
|
-
function writeLockFile(consumerRoot, lock) {
|
|
77
|
-
writeFileSync(lockFilePath(consumerRoot), `${JSON.stringify(lock, null, 2)}\n`, "utf8");
|
|
78
|
-
}
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
// Locating the consumer's Prisma schema folder via their prisma.config.ts
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
const CONSUMER_CONFIG_CANDIDATES = [
|
|
83
|
-
"prisma.config.ts",
|
|
84
|
-
"prisma.config.mts",
|
|
85
|
-
"prisma.config.js",
|
|
86
|
-
"prisma.config.mjs",
|
|
87
|
-
];
|
|
88
|
-
function findConsumerConfigFile(consumerRoot) {
|
|
89
|
-
for (const candidate of CONSUMER_CONFIG_CANDIDATES) {
|
|
90
|
-
const p = path.join(consumerRoot, candidate);
|
|
91
|
-
if (existsSync(p))
|
|
92
|
-
return p;
|
|
93
|
-
}
|
|
94
|
-
fail(`could not find a prisma.config.{ts,mts,js,mjs} in ${consumerRoot}. ` +
|
|
95
|
-
`trace-sync reads this file to find your Prisma schema folder — it does not assume a path.`);
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Best-effort read of the consumer's `schema` setting out of their
|
|
99
|
-
* prisma.config.ts default export. Supports the shapes Prisma's config API has
|
|
100
|
-
* used: a bare string path (file or folder), or `{ path: string }`. Relies on
|
|
101
|
-
* Node's native TypeScript type-stripping (unflagged as of the Node 24 baseline
|
|
102
|
-
* this package requires) to import a .ts file directly — if the consumer's
|
|
103
|
-
* config uses syntax that isn't erasable (enums, namespaces, parameter
|
|
104
|
-
* properties, etc.) this import will fail, and that failure is surfaced as-is
|
|
105
|
-
* rather than swallowed.
|
|
5
|
+
* A thin wrapper around the generalized sync engine in ./sync-engine.js — see that
|
|
6
|
+
* file's doc comment for the full subcommand reference (init/diff/apply/status) and
|
|
7
|
+
* how fragments/migrations/lock files work. This file only supplies the "trace"
|
|
8
|
+
* fragment name and the "trace-sync" bin name; profile-sync.ts is the same wrapper
|
|
9
|
+
* for the profile fragment.
|
|
106
10
|
*/
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
let mod;
|
|
110
|
-
try {
|
|
111
|
-
mod = await import(pathToFileURL(configFile).href);
|
|
112
|
-
}
|
|
113
|
-
catch (err) {
|
|
114
|
-
fail(`failed to load ${configFile}: ${err.message}\n` +
|
|
115
|
-
`If this config file uses TypeScript syntax that Node can't type-strip natively ` +
|
|
116
|
-
`(enums, namespaces, decorators, ...), trace-sync can't import it directly.`);
|
|
117
|
-
}
|
|
118
|
-
const config = mod.default ?? mod;
|
|
119
|
-
const schema = config?.schema;
|
|
120
|
-
let schemaPath;
|
|
121
|
-
if (typeof schema === "string") {
|
|
122
|
-
schemaPath = schema;
|
|
123
|
-
}
|
|
124
|
-
else if (schema &&
|
|
125
|
-
typeof schema === "object" &&
|
|
126
|
-
typeof schema.path === "string") {
|
|
127
|
-
schemaPath = schema.path;
|
|
128
|
-
}
|
|
129
|
-
if (!schemaPath) {
|
|
130
|
-
fail(`could not find a "schema" path in ${configFile}'s default export. ` +
|
|
131
|
-
`Expected \`defineConfig({ schema: "prisma/schema" })\` (or a { path } object).`);
|
|
132
|
-
}
|
|
133
|
-
return path.resolve(path.dirname(configFile), schemaPath);
|
|
134
|
-
}
|
|
135
|
-
/** Resolve the folder trace.prisma gets copied into, from the config's schema
|
|
136
|
-
* path (which may point at the folder itself, or at a single schema file
|
|
137
|
-
* inside it, e.g. "prisma/schema.prisma"). */
|
|
138
|
-
function resolveSchemaFolder(schemaPath) {
|
|
139
|
-
if (schemaPath.endsWith(".prisma")) {
|
|
140
|
-
return path.dirname(schemaPath);
|
|
141
|
-
}
|
|
142
|
-
return schemaPath;
|
|
143
|
-
}
|
|
144
|
-
/** The conventional "prisma root" a schema folder sits under, used to locate
|
|
145
|
-
* (or create) the sibling `migrations/` directory. For the common multi-file
|
|
146
|
-
* layout `prisma/schema/*.prisma`, that's `prisma/`; for a flat
|
|
147
|
-
* `prisma/schema.prisma` layout, `resolveSchemaFolder` already returned
|
|
148
|
-
* `prisma/`, so this is a no-op. */
|
|
149
|
-
function resolvePrismaRoot(schemaFolder) {
|
|
150
|
-
return path.basename(schemaFolder) === "schema"
|
|
151
|
-
? path.dirname(schemaFolder)
|
|
152
|
-
: schemaFolder;
|
|
153
|
-
}
|
|
154
|
-
// ---------------------------------------------------------------------------
|
|
155
|
-
// Shelling out to the consumer's local `prisma`
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
function runPrisma(args, cwd) {
|
|
158
|
-
// On Windows, npx resolves to npx.cmd, a shell script — node:child_process's
|
|
159
|
-
// spawnSync can't exec it directly (EINVAL) without shell:true, and shell:true
|
|
160
|
-
// with an argument array is an unescaped-concatenation footgun (path arguments
|
|
161
|
-
// here can contain spaces). cross-spawn resolves and correctly quotes .cmd/.bat
|
|
162
|
-
// executables on Windows without going through a shell at all.
|
|
163
|
-
const result = spawn.sync("npx", ["prisma", ...args], { cwd, stdio: "inherit" });
|
|
164
|
-
if (result.error) {
|
|
165
|
-
fail(`failed to run "npx prisma ${args.join(" ")}": ${result.error.message}`);
|
|
166
|
-
}
|
|
167
|
-
if (result.status !== 0) {
|
|
168
|
-
fail(`"npx prisma ${args.join(" ")}" exited with code ${result.status}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
// ---------------------------------------------------------------------------
|
|
172
|
-
// File copying
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
function copyFragment(schemaFolder) {
|
|
175
|
-
mkdirSync(schemaFolder, { recursive: true });
|
|
176
|
-
copyFileSync(fragmentSrcPath, path.join(schemaFolder, "trace.prisma"));
|
|
177
|
-
}
|
|
178
|
-
function copyMigration(migrationId, migrationsDestDir) {
|
|
179
|
-
const srcDir = path.join(migrationsSrcDir, migrationId);
|
|
180
|
-
const destDir = path.join(migrationsDestDir, migrationId);
|
|
181
|
-
mkdirSync(destDir, { recursive: true });
|
|
182
|
-
cpSync(srcDir, destDir, { recursive: true });
|
|
183
|
-
}
|
|
184
|
-
// ---------------------------------------------------------------------------
|
|
185
|
-
// Subcommands
|
|
186
|
-
// ---------------------------------------------------------------------------
|
|
187
|
-
async function cmdInit(consumerRoot) {
|
|
188
|
-
const schemaPath = await loadConsumerSchemaPath(consumerRoot);
|
|
189
|
-
const schemaFolder = resolveSchemaFolder(schemaPath);
|
|
190
|
-
const prismaRoot = resolvePrismaRoot(schemaFolder);
|
|
191
|
-
const migrationsDestDir = path.join(prismaRoot, "migrations");
|
|
192
|
-
console.log(`trace-sync: copying trace.prisma into ${schemaFolder}`);
|
|
193
|
-
copyFragment(schemaFolder);
|
|
194
|
-
console.log(`trace-sync: running "npx prisma validate" against ${consumerRoot}`);
|
|
195
|
-
runPrisma(["validate"], consumerRoot);
|
|
196
|
-
const manifest = readManifest();
|
|
197
|
-
const appliedMigrations = [];
|
|
198
|
-
for (const entry of manifest) {
|
|
199
|
-
console.log(`trace-sync: copying migration ${entry.id} into ${migrationsDestDir}`);
|
|
200
|
-
copyMigration(entry.id, migrationsDestDir);
|
|
201
|
-
const migrationSqlPath = path.join(migrationsDestDir, entry.id, "migration.sql");
|
|
202
|
-
console.log(`trace-sync: applying ${entry.id} via "npx prisma db execute"`);
|
|
203
|
-
runPrisma(["db", "execute", "--file", migrationSqlPath, "--schema", schemaFolder], consumerRoot);
|
|
204
|
-
appliedMigrations.push(entry.id);
|
|
205
|
-
}
|
|
206
|
-
const meta = readFragmentMeta();
|
|
207
|
-
writeLockFile(consumerRoot, {
|
|
208
|
-
fragmentVersion: meta.fragmentVersion,
|
|
209
|
-
fragmentChecksum: sha256File(fragmentSrcPath),
|
|
210
|
-
appliedMigrations,
|
|
211
|
-
lastSyncedAt: new Date().toISOString(),
|
|
212
|
-
});
|
|
213
|
-
console.log(`trace-sync: wrote ${lockFilePath(consumerRoot)}`);
|
|
214
|
-
console.log("trace-sync: init complete.");
|
|
215
|
-
}
|
|
216
|
-
function computePending(consumerRoot) {
|
|
217
|
-
const lock = readLockFile(consumerRoot);
|
|
218
|
-
if (!lock) {
|
|
219
|
-
fail(`no ${LOCK_FILE_NAME} found at ${consumerRoot}. Run "trace-sync init" first.`);
|
|
220
|
-
}
|
|
221
|
-
const meta = readFragmentMeta();
|
|
222
|
-
const manifest = readManifest();
|
|
223
|
-
const applied = new Set(lock.appliedMigrations);
|
|
224
|
-
const pendingMigrations = manifest.filter((entry) => !applied.has(entry.id));
|
|
225
|
-
return {
|
|
226
|
-
fragmentVersionCurrent: meta.fragmentVersion,
|
|
227
|
-
fragmentVersionInstalled: lock.fragmentVersion,
|
|
228
|
-
fragmentVersionChanged: meta.fragmentVersion !== lock.fragmentVersion,
|
|
229
|
-
pendingMigrations,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
function cmdDiff(consumerRoot) {
|
|
233
|
-
const report = computePending(consumerRoot);
|
|
234
|
-
if (report.fragmentVersionChanged) {
|
|
235
|
-
console.log(`fragment version: ${report.fragmentVersionInstalled} -> ${report.fragmentVersionCurrent} (changed)`);
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
console.log(`fragment version: ${report.fragmentVersionCurrent} (up to date)`);
|
|
239
|
-
}
|
|
240
|
-
if (report.pendingMigrations.length === 0) {
|
|
241
|
-
console.log("migrations: up to date, nothing pending.");
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
console.log(`migrations: ${report.pendingMigrations.length} pending:`);
|
|
245
|
-
for (const entry of report.pendingMigrations) {
|
|
246
|
-
console.log(` - ${entry.id} (introduced in fragment ${entry.introducedInFragmentVersion})`);
|
|
247
|
-
}
|
|
248
|
-
console.log('trace-sync diff is read-only. Run "trace-sync apply" to apply these.');
|
|
249
|
-
}
|
|
250
|
-
async function cmdApply(consumerRoot) {
|
|
251
|
-
const report = computePending(consumerRoot);
|
|
252
|
-
const schemaPath = await loadConsumerSchemaPath(consumerRoot);
|
|
253
|
-
const schemaFolder = resolveSchemaFolder(schemaPath);
|
|
254
|
-
const prismaRoot = resolvePrismaRoot(schemaFolder);
|
|
255
|
-
const migrationsDestDir = path.join(prismaRoot, "migrations");
|
|
256
|
-
console.log(`trace-sync: refreshing trace.prisma in ${schemaFolder}`);
|
|
257
|
-
copyFragment(schemaFolder);
|
|
258
|
-
const lock = readLockFile(consumerRoot);
|
|
259
|
-
if (!lock) {
|
|
260
|
-
fail(`no ${LOCK_FILE_NAME} found at ${consumerRoot}. Run "trace-sync init" first.`);
|
|
261
|
-
}
|
|
262
|
-
const appliedMigrations = [...lock.appliedMigrations];
|
|
263
|
-
if (report.pendingMigrations.length === 0) {
|
|
264
|
-
console.log("trace-sync: no pending migrations to apply.");
|
|
265
|
-
}
|
|
266
|
-
for (const entry of report.pendingMigrations) {
|
|
267
|
-
console.log(`trace-sync: copying migration ${entry.id} into ${migrationsDestDir}`);
|
|
268
|
-
copyMigration(entry.id, migrationsDestDir);
|
|
269
|
-
const migrationSqlPath = path.join(migrationsDestDir, entry.id, "migration.sql");
|
|
270
|
-
console.log(`trace-sync: applying ${entry.id} via "npx prisma db execute"`);
|
|
271
|
-
runPrisma(["db", "execute", "--file", migrationSqlPath, "--schema", schemaFolder], consumerRoot);
|
|
272
|
-
appliedMigrations.push(entry.id);
|
|
273
|
-
}
|
|
274
|
-
const meta = readFragmentMeta();
|
|
275
|
-
writeLockFile(consumerRoot, {
|
|
276
|
-
fragmentVersion: meta.fragmentVersion,
|
|
277
|
-
fragmentChecksum: sha256File(fragmentSrcPath),
|
|
278
|
-
appliedMigrations,
|
|
279
|
-
lastSyncedAt: new Date().toISOString(),
|
|
280
|
-
});
|
|
281
|
-
console.log(`trace-sync: wrote ${lockFilePath(consumerRoot)}`);
|
|
282
|
-
console.log("trace-sync: apply complete.");
|
|
283
|
-
}
|
|
284
|
-
async function cmdStatus(consumerRoot) {
|
|
285
|
-
const lock = readLockFile(consumerRoot);
|
|
286
|
-
if (!lock) {
|
|
287
|
-
console.log(`no ${LOCK_FILE_NAME} found at ${consumerRoot}. Not yet initialized — run "trace-sync init".`);
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
console.log(`fragment version (installed): ${lock.fragmentVersion}`);
|
|
291
|
-
console.log(`fragment version (package): ${readFragmentMeta().fragmentVersion}`);
|
|
292
|
-
console.log(`last synced: ${lock.lastSyncedAt}`);
|
|
293
|
-
console.log(`applied migrations (${lock.appliedMigrations.length}):`);
|
|
294
|
-
for (const id of lock.appliedMigrations) {
|
|
295
|
-
console.log(` - ${id}`);
|
|
296
|
-
}
|
|
297
|
-
try {
|
|
298
|
-
const schemaPath = await loadConsumerSchemaPath(consumerRoot);
|
|
299
|
-
const schemaFolder = resolveSchemaFolder(schemaPath);
|
|
300
|
-
const copiedFragmentPath = path.join(schemaFolder, "trace.prisma");
|
|
301
|
-
if (!existsSync(copiedFragmentPath)) {
|
|
302
|
-
console.log(`fragment copy: MISSING at ${copiedFragmentPath}`);
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
const installedChecksum = sha256File(copiedFragmentPath);
|
|
306
|
-
if (installedChecksum === lock.fragmentChecksum) {
|
|
307
|
-
console.log("fragment copy: unmodified");
|
|
308
|
-
}
|
|
309
|
-
else {
|
|
310
|
-
console.log(`fragment copy: CHECKSUM MISMATCH — ${copiedFragmentPath} has been hand-edited ` +
|
|
311
|
-
`(or is out of sync with the lock file)`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
catch (err) {
|
|
315
|
-
console.log(`fragment copy: could not verify (${err.message})`);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
// ---------------------------------------------------------------------------
|
|
319
|
-
// Entry point
|
|
320
|
-
// ---------------------------------------------------------------------------
|
|
321
|
-
const SUBCOMMANDS = ["init", "diff", "apply", "status"];
|
|
322
|
-
function isSubcommand(value) {
|
|
323
|
-
return SUBCOMMANDS.includes(value);
|
|
324
|
-
}
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { runSyncCli, SyncCliError } from "./sync-engine.js";
|
|
325
13
|
export async function main(argv) {
|
|
326
|
-
|
|
327
|
-
if (!isSubcommand(subcommand)) {
|
|
328
|
-
console.error(`usage: trace-sync <${SUBCOMMANDS.join("|")}>`);
|
|
329
|
-
process.exitCode = 1;
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
// Reserved for a future --cwd override; today trace-sync always operates on
|
|
333
|
-
// the directory it's invoked from, which is how `npx trace-sync ...` /
|
|
334
|
-
// an npm script in the consumer's own package.json will run it.
|
|
335
|
-
void rest;
|
|
336
|
-
const consumerRoot = process.cwd();
|
|
337
|
-
switch (subcommand) {
|
|
338
|
-
case "init":
|
|
339
|
-
await cmdInit(consumerRoot);
|
|
340
|
-
break;
|
|
341
|
-
case "diff":
|
|
342
|
-
cmdDiff(consumerRoot);
|
|
343
|
-
break;
|
|
344
|
-
case "apply":
|
|
345
|
-
await cmdApply(consumerRoot);
|
|
346
|
-
break;
|
|
347
|
-
case "status":
|
|
348
|
-
await cmdStatus(consumerRoot);
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
14
|
+
return runSyncCli(argv, { name: "trace", binName: "trace-sync" });
|
|
351
15
|
}
|
|
352
16
|
const isMainModule = process.argv[1] !== undefined &&
|
|
353
17
|
import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
354
18
|
if (isMainModule) {
|
|
355
19
|
main(process.argv.slice(2)).catch((err) => {
|
|
356
|
-
if (!(err instanceof
|
|
20
|
+
if (!(err instanceof SyncCliError)) {
|
|
357
21
|
console.error(err);
|
|
358
22
|
}
|
|
359
23
|
process.exitCode = process.exitCode ?? 1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace-sync.js","sourceRoot":"","sources":["../../src/cli/trace-sync.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"trace-sync.js","sourceRoot":"","sources":["../../src/cli/trace-sync.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAE5D,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAc;IACvC,OAAO,UAAU,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,YAAY,GAChB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS;IAC7B,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAE1D,IAAI,YAAY,EAAE,CAAC;IACjB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACjD,IAAI,CAAC,CAAC,GAAG,YAAY,YAAY,CAAC,EAAE,CAAC;YACnC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;QACD,OAAO,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic self-service "edit my own profile fields" module (title/phone/timezone/
|
|
3
|
+
* bio) — DEC-39, DEC-52. Ships a Prisma schema fragment + a raw SQL migration + a
|
|
4
|
+
* `profile-sync` CLI (all generalized from @flytedesk/app-kit/trace's DEC-39
|
|
5
|
+
* machinery — see prisma/fragments/profile.prisma and src/cli/profile-sync.ts) plus
|
|
6
|
+
* a real Fastify plugin with GET/PATCH routes, so flytedesk-id (and other flytedesk
|
|
7
|
+
* apps after it) don't each hand-roll their own GET/PATCH /me route + migration for
|
|
8
|
+
* this.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* import Fastify from "fastify";
|
|
13
|
+
* import { flytedeskAuth } from "@flytedesk/app-kit/auth";
|
|
14
|
+
* import { appKitProfile } from "@flytedesk/app-kit/profile";
|
|
15
|
+
*
|
|
16
|
+
* const app = Fastify();
|
|
17
|
+
* await app.register(flytedeskAuth, { ...authOptions });
|
|
18
|
+
* // Must come after flytedeskAuth (or whatever else decorates request.authUser)
|
|
19
|
+
* // — see plugin.ts's registration-time check.
|
|
20
|
+
* await app.register(appKitProfile, {
|
|
21
|
+
* store: {
|
|
22
|
+
* findByUserId: (userId) => prisma.appKitUserProfile.findUnique({ where: { userId } }),
|
|
23
|
+
* upsert: (userId, patch) =>
|
|
24
|
+
* prisma.appKitUserProfile.upsert({
|
|
25
|
+
* where: { userId },
|
|
26
|
+
* create: { userId, ...patch },
|
|
27
|
+
* update: patch,
|
|
28
|
+
* }),
|
|
29
|
+
* },
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // GET /profile -> { userId, title, phone, timezone, bio, updatedAt }
|
|
33
|
+
* // PATCH /profile { title?, phone?, timezone?, bio? } -> same shape
|
|
34
|
+
*/
|
|
35
|
+
export { appKitProfile } from "./plugin.js";
|
|
36
|
+
export type { AppKitProfileOptions, ProfileFields, ProfilePatch, ProfileResponseBody, ProfileRow, ProfileStore, } from "./types.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic self-service "edit my own profile fields" module (title/phone/timezone/
|
|
3
|
+
* bio) — DEC-39, DEC-52. Ships a Prisma schema fragment + a raw SQL migration + a
|
|
4
|
+
* `profile-sync` CLI (all generalized from @flytedesk/app-kit/trace's DEC-39
|
|
5
|
+
* machinery — see prisma/fragments/profile.prisma and src/cli/profile-sync.ts) plus
|
|
6
|
+
* a real Fastify plugin with GET/PATCH routes, so flytedesk-id (and other flytedesk
|
|
7
|
+
* apps after it) don't each hand-roll their own GET/PATCH /me route + migration for
|
|
8
|
+
* this.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* import Fastify from "fastify";
|
|
13
|
+
* import { flytedeskAuth } from "@flytedesk/app-kit/auth";
|
|
14
|
+
* import { appKitProfile } from "@flytedesk/app-kit/profile";
|
|
15
|
+
*
|
|
16
|
+
* const app = Fastify();
|
|
17
|
+
* await app.register(flytedeskAuth, { ...authOptions });
|
|
18
|
+
* // Must come after flytedeskAuth (or whatever else decorates request.authUser)
|
|
19
|
+
* // — see plugin.ts's registration-time check.
|
|
20
|
+
* await app.register(appKitProfile, {
|
|
21
|
+
* store: {
|
|
22
|
+
* findByUserId: (userId) => prisma.appKitUserProfile.findUnique({ where: { userId } }),
|
|
23
|
+
* upsert: (userId, patch) =>
|
|
24
|
+
* prisma.appKitUserProfile.upsert({
|
|
25
|
+
* where: { userId },
|
|
26
|
+
* create: { userId, ...patch },
|
|
27
|
+
* update: patch,
|
|
28
|
+
* }),
|
|
29
|
+
* },
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // GET /profile -> { userId, title, phone, timezone, bio, updatedAt }
|
|
33
|
+
* // PATCH /profile { title?, phone?, timezone?, bio? } -> same shape
|
|
34
|
+
*/
|
|
35
|
+
export { appKitProfile } from "./plugin.js";
|
|
36
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/profile/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
const DEFAULT_ROUTE_PREFIX = "/profile";
|
|
3
|
+
const PATCHABLE_FIELDS = ["title", "phone", "timezone", "bio"];
|
|
4
|
+
function assertRequiredOptions(options) {
|
|
5
|
+
if (!options.store) {
|
|
6
|
+
throw new Error('@flytedesk/app-kit/profile: missing required option "store"');
|
|
7
|
+
}
|
|
8
|
+
if (typeof options.store.findByUserId !== "function" ||
|
|
9
|
+
typeof options.store.upsert !== "function") {
|
|
10
|
+
throw new Error('@flytedesk/app-kit/profile: options.store must implement findByUserId(userId) and upsert(userId, patch)');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function toResponseBody(userId, row) {
|
|
14
|
+
if (!row) {
|
|
15
|
+
return { userId, title: null, phone: null, timezone: null, bio: null, updatedAt: null };
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
userId: row.userId,
|
|
19
|
+
title: row.title,
|
|
20
|
+
phone: row.phone,
|
|
21
|
+
timezone: row.timezone,
|
|
22
|
+
bio: row.bio,
|
|
23
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parses/validates a PATCH request body into a ProfilePatch, or returns an error
|
|
28
|
+
* message. Unknown keys are silently ignored (a permissive PATCH — only the four
|
|
29
|
+
* known fields are ever read or written); every *known* key present in the body
|
|
30
|
+
* must be a string, or the whole request is rejected with 400 rather than silently
|
|
31
|
+
* dropping the bad field.
|
|
32
|
+
*/
|
|
33
|
+
function parsePatchBody(body) {
|
|
34
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
35
|
+
return { error: "Request body must be a JSON object" };
|
|
36
|
+
}
|
|
37
|
+
const input = body;
|
|
38
|
+
const patch = {};
|
|
39
|
+
for (const field of PATCHABLE_FIELDS) {
|
|
40
|
+
if (!(field in input))
|
|
41
|
+
continue;
|
|
42
|
+
const value = input[field];
|
|
43
|
+
if (typeof value !== "string") {
|
|
44
|
+
return { error: `"${field}" must be a string` };
|
|
45
|
+
}
|
|
46
|
+
patch[field] = value;
|
|
47
|
+
}
|
|
48
|
+
return patch;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Not exported: this plugin's own minimal 401 check on `request.authUser`, kept
|
|
52
|
+
* local (rather than importing ../auth/guards.js's requireAuth) so this module has
|
|
53
|
+
* no hard dependency on the auth module's internals beyond the `authUser`
|
|
54
|
+
* decoration contract it documents (declare module "fastify" in ../auth/plugin.ts)
|
|
55
|
+
* — any consumer populating request.authUser some other way (not necessarily via
|
|
56
|
+
* @flytedesk/app-kit/auth) still works. Mirrors requireAuth's shape in
|
|
57
|
+
* ../auth/guards.ts: returns whether the request may proceed, and the route
|
|
58
|
+
* handler reads `request.authUser!` afterward, same as that module's own
|
|
59
|
+
* requirePermission does.
|
|
60
|
+
*/
|
|
61
|
+
function requireAuthUser(request, reply) {
|
|
62
|
+
if (!request.authUser) {
|
|
63
|
+
reply.code(401).send({ error: "Authentication required", code: "unauthenticated" });
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
const appKitProfilePlugin = async (fastify, options) => {
|
|
69
|
+
assertRequiredOptions(options);
|
|
70
|
+
// request.authUser is decorated by @flytedesk/app-kit/auth's flytedeskAuth plugin
|
|
71
|
+
// (see ../auth/plugin.ts's `declare module "fastify"` block) or by whatever the
|
|
72
|
+
// consuming app uses in its place. Both GET and PATCH below resolve "the current
|
|
73
|
+
// user" from request.authUser.id, so this plugin is unusable without it — rather
|
|
74
|
+
// than silently reading `undefined` and letting every request 401 in a confusing
|
|
75
|
+
// way, fail loudly and immediately at registration time if the decorator was never
|
|
76
|
+
// installed. This means: register @flytedesk/app-kit/auth (or your own equivalent)
|
|
77
|
+
// BEFORE this plugin.
|
|
78
|
+
if (!fastify.hasRequestDecorator("authUser")) {
|
|
79
|
+
throw new Error('@flytedesk/app-kit/profile: request.authUser is not decorated on this Fastify instance yet. ' +
|
|
80
|
+
"This plugin resolves the caller from request.authUser and must be registered AFTER " +
|
|
81
|
+
"whatever populates it — @flytedesk/app-kit/auth's flytedeskAuth plugin, or the " +
|
|
82
|
+
"consuming app's own equivalent decorator.");
|
|
83
|
+
}
|
|
84
|
+
const routePrefix = options.routePrefix ?? DEFAULT_ROUTE_PREFIX;
|
|
85
|
+
const store = options.store;
|
|
86
|
+
/** GET {routePrefix} — the caller's own profile. Never 404s: a user with no row
|
|
87
|
+
* yet still gets a 200 with every field null (see ProfileStore.findByUserId). */
|
|
88
|
+
fastify.get(routePrefix, async (request, reply) => {
|
|
89
|
+
if (!requireAuthUser(request, reply))
|
|
90
|
+
return;
|
|
91
|
+
const row = await store.findByUserId(request.authUser.id);
|
|
92
|
+
return toResponseBody(request.authUser.id, row);
|
|
93
|
+
});
|
|
94
|
+
/** PATCH {routePrefix} — upserts whatever subset of {title, phone, timezone, bio}
|
|
95
|
+
* is present in the body, scoped to the caller's own row (request.authUser.id is
|
|
96
|
+
* the only id this handler ever reads or writes — never a caller-supplied one). */
|
|
97
|
+
fastify.patch(routePrefix, async (request, reply) => {
|
|
98
|
+
if (!requireAuthUser(request, reply))
|
|
99
|
+
return;
|
|
100
|
+
const parsed = parsePatchBody(request.body);
|
|
101
|
+
if ("error" in parsed) {
|
|
102
|
+
return reply.code(400).send({ error: parsed.error, code: "invalid_body" });
|
|
103
|
+
}
|
|
104
|
+
const row = await store.upsert(request.authUser.id, parsed);
|
|
105
|
+
return toResponseBody(request.authUser.id, row);
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
export const appKitProfile = fp(appKitProfilePlugin, {
|
|
109
|
+
name: "@flytedesk/app-kit/profile",
|
|
110
|
+
});
|
|
111
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/profile/plugin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-written row/option types for @flytedesk/app-kit/profile.
|
|
3
|
+
*
|
|
4
|
+
* Field names here mirror prisma/fragments/profile.prisma's camelCase model fields
|
|
5
|
+
* (userId/title/phone/timezone/bio/updatedAt) — a consumer's generated PrismaClient,
|
|
6
|
+
* once they've run `profile-sync init` and their own `prisma generate`, satisfies
|
|
7
|
+
* ProfileStore directly via `prisma.appKitUserProfile.findUnique` / `.upsert`.
|
|
8
|
+
*/
|
|
9
|
+
export interface ProfileRow {
|
|
10
|
+
userId: string;
|
|
11
|
+
title: string | null;
|
|
12
|
+
phone: string | null;
|
|
13
|
+
timezone: string | null;
|
|
14
|
+
bio: string | null;
|
|
15
|
+
updatedAt: Date;
|
|
16
|
+
}
|
|
17
|
+
/** The self-service fields a caller may write. */
|
|
18
|
+
export type ProfileFields = Pick<ProfileRow, "title" | "phone" | "timezone" | "bio">;
|
|
19
|
+
/** A PATCH body: any subset of ProfileFields, all still required to be strings
|
|
20
|
+
* (or absent) once validated — see parsePatchBody in ./plugin.ts. */
|
|
21
|
+
export type ProfilePatch = Partial<ProfileFields>;
|
|
22
|
+
/**
|
|
23
|
+
* Minimal persistence seam this plugin needs — deliberately NOT a dependency on
|
|
24
|
+
* `@prisma/client` (this package never builds its own PrismaClient; mirrors why
|
|
25
|
+
* src/trace/types.ts's TracePrismaClient and src/auth/types.ts's AuthStore are both
|
|
26
|
+
* ORM-agnostic structural interfaces rather than importing the generated client
|
|
27
|
+
* type). A consumer's generated PrismaClient trivially satisfies this shape:
|
|
28
|
+
*
|
|
29
|
+
* const store: ProfileStore = {
|
|
30
|
+
* findByUserId: (userId) => prisma.appKitUserProfile.findUnique({ where: { userId } }),
|
|
31
|
+
* upsert: (userId, patch) =>
|
|
32
|
+
* prisma.appKitUserProfile.upsert({
|
|
33
|
+
* where: { userId },
|
|
34
|
+
* create: { userId, ...patch },
|
|
35
|
+
* update: patch,
|
|
36
|
+
* }),
|
|
37
|
+
* };
|
|
38
|
+
*
|
|
39
|
+
* A hand-rolled adapter can back it with anything else (raw SQL, a different ORM,
|
|
40
|
+
* an in-memory store for tests — see src/profile/plugin.test.ts).
|
|
41
|
+
*/
|
|
42
|
+
export interface ProfileStore {
|
|
43
|
+
/** Must return `null`, not throw/404, when the user has no row yet — a person
|
|
44
|
+
* with no profile row still "has a profile," it's just all-null fields. */
|
|
45
|
+
findByUserId(userId: string): Promise<ProfileRow | null>;
|
|
46
|
+
/** Upserts the row for `userId`, writing only the fields present in `patch`
|
|
47
|
+
* (fields absent from `patch` are left unchanged on an existing row, and null
|
|
48
|
+
* on a newly-created one). */
|
|
49
|
+
upsert(userId: string, patch: ProfilePatch): Promise<ProfileRow>;
|
|
50
|
+
}
|
|
51
|
+
export interface AppKitProfileOptions {
|
|
52
|
+
/** The persistence seam — see ProfileStore above. Required. */
|
|
53
|
+
store: ProfileStore;
|
|
54
|
+
/**
|
|
55
|
+
* Base path the GET/PATCH routes are mounted under. Default "/profile".
|
|
56
|
+
*
|
|
57
|
+
* Deliberately not "/me": @flytedesk/app-kit/auth's own README/index.ts doc
|
|
58
|
+
* comment illustrates a consumer wiring up `GET /me` themselves, returning
|
|
59
|
+
* `request.authUser` (identity/roles/permissions — see src/auth/index.ts). That's
|
|
60
|
+
* a different resource than this plugin's editable title/phone/timezone/bio
|
|
61
|
+
* fields, so reusing "/me" here would be conceptually confusing even though nothing
|
|
62
|
+
* actually collides at registration time (auth doesn't register that route itself,
|
|
63
|
+
* it's just the example a consumer is expected to write).
|
|
64
|
+
*/
|
|
65
|
+
routePrefix?: string;
|
|
66
|
+
}
|
|
67
|
+
/** What GET/PATCH `{routePrefix}` return. Always 200 — see ProfileStore.findByUserId. */
|
|
68
|
+
export interface ProfileResponseBody {
|
|
69
|
+
userId: string;
|
|
70
|
+
title: string | null;
|
|
71
|
+
phone: string | null;
|
|
72
|
+
timezone: string | null;
|
|
73
|
+
bio: string | null;
|
|
74
|
+
/** ISO 8601, or `null` if the user has no row yet. */
|
|
75
|
+
updatedAt: string | null;
|
|
76
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-written row/option types for @flytedesk/app-kit/profile.
|
|
3
|
+
*
|
|
4
|
+
* Field names here mirror prisma/fragments/profile.prisma's camelCase model fields
|
|
5
|
+
* (userId/title/phone/timezone/bio/updatedAt) — a consumer's generated PrismaClient,
|
|
6
|
+
* once they've run `profile-sync init` and their own `prisma generate`, satisfies
|
|
7
|
+
* ProfileStore directly via `prisma.appKitUserProfile.findUnique` / `.upsert`.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/profile/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flytedesk/app-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Shared platform kit for flytedesk apps: flytedesk-id auth (BFF/OIDC client) and a Postgres-native trace/audit layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"scripts"
|
|
29
29
|
],
|
|
30
30
|
"bin": {
|
|
31
|
-
"trace-sync": "./dist/cli/trace-sync.js"
|
|
31
|
+
"trace-sync": "./dist/cli/trace-sync.js",
|
|
32
|
+
"profile-sync": "./dist/cli/profile-sync.js"
|
|
32
33
|
},
|
|
33
34
|
"exports": {
|
|
34
35
|
"./auth": {
|
|
@@ -42,6 +43,10 @@
|
|
|
42
43
|
"./rate-limit": {
|
|
43
44
|
"types": "./dist/rate-limit/index.d.ts",
|
|
44
45
|
"import": "./dist/rate-limit/index.js"
|
|
46
|
+
},
|
|
47
|
+
"./profile": {
|
|
48
|
+
"types": "./dist/profile/index.d.ts",
|
|
49
|
+
"import": "./dist/profile/index.js"
|
|
45
50
|
}
|
|
46
51
|
},
|
|
47
52
|
"scripts": {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @flytedesk/app-kit/profile — canonical Prisma model fragment (DEC-39, ported to a
|
|
2
|
+
// second fragment for the profile module).
|
|
3
|
+
//
|
|
4
|
+
// This fragment is NOT built into a Prisma Client by this package. It is copied
|
|
5
|
+
// verbatim into the consumer's own multi-file Prisma schema folder by `profile-sync
|
|
6
|
+
// init` (see src/cli/profile-sync.ts, which — like trace-sync — is a thin wrapper
|
|
7
|
+
// around the generalized src/cli/sync-engine.ts), and the consumer generates their
|
|
8
|
+
// own client against their own schema + this fragment merged in.
|
|
9
|
+
//
|
|
10
|
+
// What this models: title/phone/timezone/bio are cross-app, person-level facts —
|
|
11
|
+
// not specific to any one flytedesk product. The first real consumer is
|
|
12
|
+
// flytedesk-id, replacing its own local per-field storage for these with this
|
|
13
|
+
// shared table so other flytedesk apps don't each hand-roll their own GET/PATCH
|
|
14
|
+
// /me route + migration for the same handful of self-service profile fields.
|
|
15
|
+
//
|
|
16
|
+
// Design constraints (deliberate, see DEC-39 — same reasoning as trace.prisma):
|
|
17
|
+
// - No `@relation`/FK attributes anywhere. `userId` is a plain scalar matching
|
|
18
|
+
// the consuming app's own user-id type by convention, not a foreign key — a
|
|
19
|
+
// package that ships models into a consumer's schema cannot see the
|
|
20
|
+
// consumer's own User model to point a relation at.
|
|
21
|
+
// - No Prisma `enum`. None of these fields need one today, but the same
|
|
22
|
+
// schema-global-collision reasoning as trace.prisma applies if one is ever
|
|
23
|
+
// added here: it would stay a plain `String` column instead.
|
|
24
|
+
// - `String @id` (not `BigInt @default(autoincrement())`): unlike trace's
|
|
25
|
+
// high-write append log, this table has exactly one row per user, keyed
|
|
26
|
+
// directly on the consumer's own user id — no separate surrogate key earns
|
|
27
|
+
// its keep here.
|
|
28
|
+
//
|
|
29
|
+
// Table name is prefixed `app_kit_` via `@@map` so it can't collide with a
|
|
30
|
+
// consumer's own tables. Column names are snake_case via `@map`, matching the raw
|
|
31
|
+
// SQL in prisma/migrations/0002_app_kit_profile_init/migration.sql exactly.
|
|
32
|
+
|
|
33
|
+
model AppKitUserProfile {
|
|
34
|
+
userId String @id @map("user_id")
|
|
35
|
+
title String?
|
|
36
|
+
phone String?
|
|
37
|
+
timezone String?
|
|
38
|
+
bio String?
|
|
39
|
+
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
|
40
|
+
|
|
41
|
+
@@map("app_kit_user_profile")
|
|
42
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- @flytedesk/app-kit/profile :: 0002_app_kit_profile_init
|
|
2
|
+
--
|
|
3
|
+
-- Hand-written raw SQL (this package does not run `prisma migrate dev` against its
|
|
4
|
+
-- own schema — see DEC-39, and 0001_app_kit_trace_init's migration.sql for the same
|
|
5
|
+
-- note on the trace fragment). Column names, types, and defaults match
|
|
6
|
+
-- prisma/fragments/profile.prisma exactly: TEXT for the String @id and the nullable
|
|
7
|
+
-- String columns, TIMESTAMPTZ for DateTime.
|
|
8
|
+
CREATE TABLE app_kit_user_profile (
|
|
9
|
+
user_id TEXT PRIMARY KEY,
|
|
10
|
+
title TEXT,
|
|
11
|
+
phone TEXT,
|
|
12
|
+
timezone TEXT,
|
|
13
|
+
bio TEXT,
|
|
14
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
15
|
+
);
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
3
|
"id": "0001_app_kit_trace_init",
|
|
4
|
+
"fragment": "trace",
|
|
4
5
|
"file": "0001_app_kit_trace_init/migration.sql",
|
|
5
6
|
"checksum": "sha256:413918649af395583a2647f415ae4c8249477a922f1f5e25916742c535253915",
|
|
6
7
|
"introducedInFragmentVersion": "1.0.0"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "0002_app_kit_profile_init",
|
|
11
|
+
"fragment": "profile",
|
|
12
|
+
"file": "0002_app_kit_profile_init/migration.sql",
|
|
13
|
+
"checksum": "sha256:62d688a2db89c77f12c61a3c98be5cfe84a6e06f8a4dda9d24b0f9acd5511b32",
|
|
14
|
+
"introducedInFragmentVersion": "1.0.0"
|
|
7
15
|
}
|
|
8
16
|
]
|