@flytedesk/app-kit 0.1.3 → 0.3.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/flags-sync.d.ts +2 -0
- package/dist/cli/flags-sync.js +26 -0
- package/dist/cli/flags-sync.js.map +1 -0
- 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 +543 -0
- package/dist/cli/sync-engine.js.map +1 -0
- package/dist/cli/trace-sync.js +9 -359
- package/dist/cli/trace-sync.js.map +1 -1
- package/dist/flags/index.d.ts +49 -0
- package/dist/flags/index.js +49 -0
- package/dist/flags/index.js.map +1 -0
- package/dist/flags/plugin.d.ts +3 -0
- package/dist/flags/plugin.js +296 -0
- package/dist/flags/plugin.js.map +1 -0
- package/dist/flags/types.d.ts +159 -0
- package/dist/flags/types.js +11 -0
- package/dist/flags/types.js.map +1 -0
- 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 +12 -2
- package/prisma/fragments/flags.meta.json +4 -0
- package/prisma/fragments/flags.prisma +52 -0
- 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/0003_app_kit_flags_init/migration.sql +16 -0
- package/prisma/migrations/manifest.json +15 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
const DEFAULT_ROUTE_PREFIX = "/flags";
|
|
3
|
+
const DEFAULT_PAGE_SIZE = 25;
|
|
4
|
+
const MAX_PAGE_SIZE = 200;
|
|
5
|
+
function assertRequiredOptions(options) {
|
|
6
|
+
if (!options.store) {
|
|
7
|
+
throw new Error('@flytedesk/app-kit/flags: missing required option "store"');
|
|
8
|
+
}
|
|
9
|
+
if (typeof options.store.list !== "function" ||
|
|
10
|
+
typeof options.store.findByKey !== "function" ||
|
|
11
|
+
typeof options.store.upsert !== "function") {
|
|
12
|
+
throw new Error("@flytedesk/app-kit/flags: options.store must implement list(), findByKey(key), and upsert(key, patch)");
|
|
13
|
+
}
|
|
14
|
+
if (typeof options.describeFlag !== "function") {
|
|
15
|
+
throw new Error('@flytedesk/app-kit/flags: missing required option "describeFlag"');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function toFlagResponse(row, meta) {
|
|
19
|
+
return {
|
|
20
|
+
key: row.key,
|
|
21
|
+
project: meta.project,
|
|
22
|
+
label: meta.label,
|
|
23
|
+
description: meta.description,
|
|
24
|
+
enabled: row.enabled,
|
|
25
|
+
rollout: row.rollout,
|
|
26
|
+
roles: row.roles,
|
|
27
|
+
users: row.users,
|
|
28
|
+
owner: meta.owner,
|
|
29
|
+
updated: row.updatedAt.toISOString(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Not exported: this plugin's own minimal 401 check on `request.authUser`, kept
|
|
34
|
+
* local rather than importing ../auth/guards.js's requireAuth — identical contract
|
|
35
|
+
* and identical implementation to src/profile/plugin.ts's own requireAuthUser (see
|
|
36
|
+
* that file's doc comment for the full rationale: no hard dependency on the auth
|
|
37
|
+
* module's internals beyond the `authUser` decoration contract, so any consumer
|
|
38
|
+
* populating request.authUser some other way still works).
|
|
39
|
+
*/
|
|
40
|
+
function requireAuthUser(request, reply) {
|
|
41
|
+
if (!request.authUser) {
|
|
42
|
+
reply
|
|
43
|
+
.code(401)
|
|
44
|
+
.send({ error: "Authentication required", code: "unauthenticated" });
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
function coercePositiveInt(value, fallback, max) {
|
|
50
|
+
const n = typeof value === "string"
|
|
51
|
+
? Number(value)
|
|
52
|
+
: typeof value === "number"
|
|
53
|
+
? value
|
|
54
|
+
: NaN;
|
|
55
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1)
|
|
56
|
+
return fallback;
|
|
57
|
+
return max != null ? Math.min(n, max) : n;
|
|
58
|
+
}
|
|
59
|
+
function parseListQuery(query) {
|
|
60
|
+
return {
|
|
61
|
+
page: coercePositiveInt(query.page, 1),
|
|
62
|
+
pageSize: coercePositiveInt(query.pageSize, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE),
|
|
63
|
+
sort: typeof query.sort === "string" ? query.sort : undefined,
|
|
64
|
+
dir: query.dir === "asc" ? "asc" : "desc",
|
|
65
|
+
q: typeof query.q === "string" ? query.q : "",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Repeated querystring keys (?project=a&project=b) arrive as arrays already —
|
|
69
|
+
* Fastify's querystring parser gives us those natively, same as the reference's
|
|
70
|
+
* own asArray relies on. */
|
|
71
|
+
function asArray(value) {
|
|
72
|
+
if (value == null)
|
|
73
|
+
return [];
|
|
74
|
+
if (Array.isArray(value))
|
|
75
|
+
return value.filter((v) => typeof v === "string");
|
|
76
|
+
return typeof value === "string" ? [value] : [];
|
|
77
|
+
}
|
|
78
|
+
function buildPager(page, pageSize, total) {
|
|
79
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
80
|
+
const clampedPage = Math.min(Math.max(1, page), totalPages);
|
|
81
|
+
return {
|
|
82
|
+
page: clampedPage,
|
|
83
|
+
pageSize,
|
|
84
|
+
total,
|
|
85
|
+
totalPages,
|
|
86
|
+
from: total === 0 ? 0 : (clampedPage - 1) * pageSize + 1,
|
|
87
|
+
to: Math.min(total, clampedPage * pageSize),
|
|
88
|
+
hasPrev: clampedPage > 1,
|
|
89
|
+
hasNext: clampedPage < totalPages,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// PATCH body validation — hand-written for the same no-new-dependency reason as
|
|
94
|
+
// the list query parsing above. Same fields/constraints as the reference's zod
|
|
95
|
+
// schema: enabled (boolean), rollout (integer 0-100), roles (string[]), users
|
|
96
|
+
// (object of string -> boolean), all optional.
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
function parsePatchBody(body) {
|
|
99
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
100
|
+
return { error: "Request body must be a JSON object" };
|
|
101
|
+
}
|
|
102
|
+
const input = body;
|
|
103
|
+
const patch = {};
|
|
104
|
+
if ("enabled" in input) {
|
|
105
|
+
if (typeof input.enabled !== "boolean") {
|
|
106
|
+
return { error: '"enabled" must be a boolean' };
|
|
107
|
+
}
|
|
108
|
+
patch.enabled = input.enabled;
|
|
109
|
+
}
|
|
110
|
+
if ("rollout" in input) {
|
|
111
|
+
const rollout = input.rollout;
|
|
112
|
+
if (typeof rollout !== "number" ||
|
|
113
|
+
!Number.isInteger(rollout) ||
|
|
114
|
+
rollout < 0 ||
|
|
115
|
+
rollout > 100) {
|
|
116
|
+
return { error: '"rollout" must be an integer between 0 and 100' };
|
|
117
|
+
}
|
|
118
|
+
patch.rollout = rollout;
|
|
119
|
+
}
|
|
120
|
+
if ("roles" in input) {
|
|
121
|
+
const roles = input.roles;
|
|
122
|
+
if (!Array.isArray(roles) || roles.some((r) => typeof r !== "string")) {
|
|
123
|
+
return { error: '"roles" must be an array of strings' };
|
|
124
|
+
}
|
|
125
|
+
patch.roles = roles;
|
|
126
|
+
}
|
|
127
|
+
if ("users" in input) {
|
|
128
|
+
const users = input.users;
|
|
129
|
+
if (typeof users !== "object" ||
|
|
130
|
+
users === null ||
|
|
131
|
+
Array.isArray(users) ||
|
|
132
|
+
Object.values(users).some((v) => typeof v !== "boolean")) {
|
|
133
|
+
return {
|
|
134
|
+
error: '"users" must be an object mapping user ids to booleans',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
patch.users = users;
|
|
138
|
+
}
|
|
139
|
+
return patch;
|
|
140
|
+
}
|
|
141
|
+
const appKitFlagsPlugin = async (fastify, options) => {
|
|
142
|
+
assertRequiredOptions(options);
|
|
143
|
+
// Same registration-time contract as src/profile/plugin.ts — see that file's
|
|
144
|
+
// matching check for the full rationale. Register @flytedesk/app-kit/auth (or
|
|
145
|
+
// your own equivalent) BEFORE this plugin.
|
|
146
|
+
if (!fastify.hasRequestDecorator("authUser")) {
|
|
147
|
+
throw new Error("@flytedesk/app-kit/flags: request.authUser is not decorated on this Fastify instance yet. " +
|
|
148
|
+
"This plugin resolves the caller from request.authUser and must be registered AFTER " +
|
|
149
|
+
"whatever populates it — @flytedesk/app-kit/auth's flytedeskAuth plugin, or the " +
|
|
150
|
+
"consuming app's own equivalent decorator.");
|
|
151
|
+
}
|
|
152
|
+
const routePrefix = options.routePrefix ?? DEFAULT_ROUTE_PREFIX;
|
|
153
|
+
const store = options.store;
|
|
154
|
+
const describeFlag = options.describeFlag;
|
|
155
|
+
/**
|
|
156
|
+
* GET {routePrefix} — search/filter/sort/paginate every flag that has metadata
|
|
157
|
+
* (ported from the reference GET /flags: same q/project/state filters, same
|
|
158
|
+
* project-then-arbitrary-column sort, same facet counts over the filtered set
|
|
159
|
+
* before pagination is applied). A flag with no `describeFlag` metadata is
|
|
160
|
+
* excluded entirely, matching the reference's `.filter((x) => x.meta)`.
|
|
161
|
+
*
|
|
162
|
+
* No permission check baked in here — media-planner's own `flags.manage` is its
|
|
163
|
+
* own RBAC category, entirely the consumer's concern via their own `preHandler`
|
|
164
|
+
* on this route (same boundary /profile draws around authorization).
|
|
165
|
+
*/
|
|
166
|
+
fastify.get(routePrefix, async (request, reply) => {
|
|
167
|
+
if (!requireAuthUser(request, reply))
|
|
168
|
+
return;
|
|
169
|
+
const query = request.query;
|
|
170
|
+
const parsedQuery = parseListQuery(query);
|
|
171
|
+
const projectFilter = asArray(query.project);
|
|
172
|
+
const stateFilter = asArray(query.state);
|
|
173
|
+
const rows = await store.list();
|
|
174
|
+
let joined = rows
|
|
175
|
+
.map((row) => ({ row, meta: describeFlag(row.key) }))
|
|
176
|
+
.filter((x) => x.meta !== undefined);
|
|
177
|
+
if (parsedQuery.q) {
|
|
178
|
+
const term = parsedQuery.q.toLowerCase();
|
|
179
|
+
joined = joined.filter((x) => x.row.key.toLowerCase().includes(term) ||
|
|
180
|
+
x.meta.label.toLowerCase().includes(term) ||
|
|
181
|
+
x.meta.description.toLowerCase().includes(term) ||
|
|
182
|
+
x.meta.owner.toLowerCase().includes(term));
|
|
183
|
+
}
|
|
184
|
+
if (projectFilter.length) {
|
|
185
|
+
joined = joined.filter((x) => projectFilter.includes(x.meta.project));
|
|
186
|
+
}
|
|
187
|
+
if (stateFilter.length) {
|
|
188
|
+
joined = joined.filter((x) => stateFilter.includes(x.row.enabled ? "on" : "off"));
|
|
189
|
+
}
|
|
190
|
+
const projectFacets = {};
|
|
191
|
+
const stateFacets = {};
|
|
192
|
+
for (const x of joined) {
|
|
193
|
+
projectFacets[x.meta.project] = (projectFacets[x.meta.project] ?? 0) + 1;
|
|
194
|
+
const stateKey = x.row.enabled ? "on" : "off";
|
|
195
|
+
stateFacets[stateKey] = (stateFacets[stateKey] ?? 0) + 1;
|
|
196
|
+
}
|
|
197
|
+
const sortKey = parsedQuery.sort ?? "project";
|
|
198
|
+
joined.sort((a, b) => {
|
|
199
|
+
const av = sortKey === "project"
|
|
200
|
+
? a.meta.project
|
|
201
|
+
: a.row[sortKey];
|
|
202
|
+
const bv = sortKey === "project"
|
|
203
|
+
? b.meta.project
|
|
204
|
+
: b.row[sortKey];
|
|
205
|
+
const cmp = String(av).localeCompare(String(bv));
|
|
206
|
+
return parsedQuery.dir === "asc" ? cmp : -cmp;
|
|
207
|
+
});
|
|
208
|
+
const total = joined.length;
|
|
209
|
+
const page = Math.min(Math.max(1, parsedQuery.page), Math.max(1, Math.ceil(total / parsedQuery.pageSize)));
|
|
210
|
+
const paged = joined.slice((page - 1) * parsedQuery.pageSize, page * parsedQuery.pageSize);
|
|
211
|
+
const body = {
|
|
212
|
+
rows: paged.map((x) => toFlagResponse(x.row, x.meta)),
|
|
213
|
+
pager: buildPager(page, parsedQuery.pageSize, total),
|
|
214
|
+
sort: { key: sortKey, dir: parsedQuery.dir },
|
|
215
|
+
filter: {
|
|
216
|
+
applied: { project: projectFilter, state: stateFilter },
|
|
217
|
+
q: parsedQuery.q,
|
|
218
|
+
facets: { project: projectFacets, state: stateFacets },
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
return body;
|
|
222
|
+
});
|
|
223
|
+
/**
|
|
224
|
+
* GET {routePrefix}/:key — single flag lookup. Not present in the reference
|
|
225
|
+
* flags.ts, but a natural, trivial addition given FlagStore already exposes
|
|
226
|
+
* findByKey — completing the CRUD surface the store interface implies, not scope
|
|
227
|
+
* creep. 404s when the key has no store row OR no describeFlag metadata, so this
|
|
228
|
+
* agrees with the list endpoint about what counts as "a flag" rather than
|
|
229
|
+
* exposing a key here that the list silently hides.
|
|
230
|
+
*/
|
|
231
|
+
fastify.get(`${routePrefix}/:key`, async (request, reply) => {
|
|
232
|
+
if (!requireAuthUser(request, reply))
|
|
233
|
+
return;
|
|
234
|
+
const meta = describeFlag(request.params.key);
|
|
235
|
+
if (!meta) {
|
|
236
|
+
return reply.code(404).send({ error: "Not found", code: "not_found" });
|
|
237
|
+
}
|
|
238
|
+
const row = await store.findByKey(request.params.key);
|
|
239
|
+
if (!row) {
|
|
240
|
+
return reply.code(404).send({ error: "Not found", code: "not_found" });
|
|
241
|
+
}
|
|
242
|
+
return toFlagResponse(row, meta);
|
|
243
|
+
});
|
|
244
|
+
/**
|
|
245
|
+
* PATCH {routePrefix}/:key — partial update of enabled/rollout/roles/users
|
|
246
|
+
* (ported from the reference PATCH /flags/:flagKey: same validation, same
|
|
247
|
+
* only-write-what-changed semantics). Requires the same describeFlag metadata as
|
|
248
|
+
* GET/:key (a deliberate generalization beyond the reference, which never
|
|
249
|
+
* consulted FLAG_ROADMAP for PATCH at all — see FlagMeta's doc comment on why: a
|
|
250
|
+
* uniform "no metadata means it isn't a flag through this API" contract across
|
|
251
|
+
* every route is more predictable for a package with multiple consumers than
|
|
252
|
+
* replicating that one route's inconsistency).
|
|
253
|
+
*
|
|
254
|
+
* Always calls `options.onChange` after a successful update, even when the patch
|
|
255
|
+
* changed nothing — mirrors the reference's own unconditional `recordAudit` call
|
|
256
|
+
* (whose `detail` falls back to "No change" rather than being skipped).
|
|
257
|
+
*/
|
|
258
|
+
fastify.patch(`${routePrefix}/:key`, async (request, reply) => {
|
|
259
|
+
if (!requireAuthUser(request, reply))
|
|
260
|
+
return;
|
|
261
|
+
const parsed = parsePatchBody(request.body);
|
|
262
|
+
if ("error" in parsed) {
|
|
263
|
+
return reply
|
|
264
|
+
.code(400)
|
|
265
|
+
.send({ error: parsed.error, code: "invalid_body" });
|
|
266
|
+
}
|
|
267
|
+
const meta = describeFlag(request.params.key);
|
|
268
|
+
const existing = meta ? await store.findByKey(request.params.key) : null;
|
|
269
|
+
if (!meta || !existing) {
|
|
270
|
+
return reply.code(404).send({ error: "Not found", code: "not_found" });
|
|
271
|
+
}
|
|
272
|
+
const changes = {};
|
|
273
|
+
if (parsed.enabled !== undefined && parsed.enabled !== existing.enabled) {
|
|
274
|
+
changes.enabled = parsed.enabled;
|
|
275
|
+
}
|
|
276
|
+
if (parsed.rollout !== undefined && parsed.rollout !== existing.rollout) {
|
|
277
|
+
changes.rollout = parsed.rollout;
|
|
278
|
+
}
|
|
279
|
+
if (parsed.roles !== undefined) {
|
|
280
|
+
changes.roles = parsed.roles;
|
|
281
|
+
}
|
|
282
|
+
if (parsed.users !== undefined) {
|
|
283
|
+
changes.users = parsed.users;
|
|
284
|
+
}
|
|
285
|
+
const updated = await store.upsert(existing.key, changes);
|
|
286
|
+
await options.onChange?.(existing.key, changes, {
|
|
287
|
+
id: request.authUser.id,
|
|
288
|
+
name: request.authUser.name,
|
|
289
|
+
});
|
|
290
|
+
return toFlagResponse(updated, meta);
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
export const appKitFlags = fp(appKitFlagsPlugin, {
|
|
294
|
+
name: "@flytedesk/app-kit/flags",
|
|
295
|
+
});
|
|
296
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/flags/plugin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-written row/option types for @flytedesk/app-kit/flags.
|
|
3
|
+
*
|
|
4
|
+
* Field names here mirror prisma/fragments/flags.prisma's camelCase model fields
|
|
5
|
+
* (key/project/enabled/rollout/roles/users/updatedAt) — a consumer's generated
|
|
6
|
+
* PrismaClient, once they've run `flags-sync init` and their own `prisma generate`,
|
|
7
|
+
* satisfies FlagStore directly via `prisma.appKitFeatureFlag.findMany` /
|
|
8
|
+
* `.findUnique` / `.upsert`.
|
|
9
|
+
*/
|
|
10
|
+
export interface FlagRow {
|
|
11
|
+
key: string;
|
|
12
|
+
/** The row's own project column (mirrors media-planner's FeatureFlag.project
|
|
13
|
+
* exactly). NOT what list/GET responses display as `project`, or what the
|
|
14
|
+
* `project` list filter matches against — see FlagMeta.project below and
|
|
15
|
+
* plugin.ts's doc comment on why. */
|
|
16
|
+
project: string;
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
rollout: number;
|
|
19
|
+
/** Opaque role ids from whatever IdP/RBAC system the consumer uses — this
|
|
20
|
+
* package has no opinion on what they mean, same as media-planner's own
|
|
21
|
+
* FeatureFlag.roles. */
|
|
22
|
+
roles: string[];
|
|
23
|
+
/** Per-user override bag, keyed by whatever user id the consumer uses, each
|
|
24
|
+
* value a hard on/off override that supersedes `enabled`/`rollout`/`roles` for
|
|
25
|
+
* that one user. Evaluating overrides against a specific caller is the
|
|
26
|
+
* consumer's own concern (same boundary media-planner draws) — this plugin only
|
|
27
|
+
* ever reads/writes the bag as a whole, never evaluates it. */
|
|
28
|
+
users: Record<string, boolean>;
|
|
29
|
+
updatedAt: Date;
|
|
30
|
+
}
|
|
31
|
+
/** The fields a PATCH may write — all optional, only the ones present in the
|
|
32
|
+
* request body (and that actually differ from the current row) are written. */
|
|
33
|
+
export type FlagFields = Pick<FlagRow, "enabled" | "rollout" | "roles" | "users">;
|
|
34
|
+
export type FlagPatch = Partial<FlagFields>;
|
|
35
|
+
/**
|
|
36
|
+
* Flag metadata a consumer resolves from its own source of truth (media-planner's
|
|
37
|
+
* own reference implementation used a hardcoded `FLAG_ROADMAP` array) — the one
|
|
38
|
+
* genuinely app-specific piece of the reference `flags.ts` this module generalizes
|
|
39
|
+
* away via `AppKitFlagsOptions.describeFlag`. `project` here (NOT `FlagRow.project`)
|
|
40
|
+
* is what every route actually filters/sorts/displays as "the flag's project" —
|
|
41
|
+
* ported faithfully from the reference, which reads `meta.project` everywhere and
|
|
42
|
+
* never reads the DB row's own `project` column.
|
|
43
|
+
*/
|
|
44
|
+
export interface FlagMeta {
|
|
45
|
+
label: string;
|
|
46
|
+
description: string;
|
|
47
|
+
owner: string;
|
|
48
|
+
project: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Minimal persistence seam this plugin needs — deliberately NOT a dependency on
|
|
52
|
+
* `@prisma/client`, mirroring every other app-kit module's *Store interface (see
|
|
53
|
+
* src/profile/types.ts's ProfileStore, src/trace/types.ts's TracePrismaClient). A
|
|
54
|
+
* consumer's generated PrismaClient trivially satisfies this shape:
|
|
55
|
+
*
|
|
56
|
+
* const store: FlagStore = {
|
|
57
|
+
* list: () => prisma.appKitFeatureFlag.findMany(),
|
|
58
|
+
* findByKey: (key) => prisma.appKitFeatureFlag.findUnique({ where: { key } }),
|
|
59
|
+
* upsert: (key, patch) =>
|
|
60
|
+
* prisma.appKitFeatureFlag.upsert({
|
|
61
|
+
* where: { key },
|
|
62
|
+
* create: { key, project: "unknown", ...patch },
|
|
63
|
+
* update: patch,
|
|
64
|
+
* }),
|
|
65
|
+
* };
|
|
66
|
+
*
|
|
67
|
+
* A hand-rolled adapter can back it with anything else (raw SQL, a different ORM,
|
|
68
|
+
* an in-memory store for tests — see src/flags/plugin.test.ts).
|
|
69
|
+
*/
|
|
70
|
+
export interface FlagStore {
|
|
71
|
+
list(): Promise<FlagRow[]>;
|
|
72
|
+
/** Must return `null`, not throw/404, when no row exists for `key`. */
|
|
73
|
+
findByKey(key: string): Promise<FlagRow | null>;
|
|
74
|
+
/** Upserts the row for `key`, writing only the fields present in `patch` (fields
|
|
75
|
+
* absent from `patch` are left unchanged on an existing row). This plugin only
|
|
76
|
+
* ever calls this on a key it already confirmed exists via findByKey first (see
|
|
77
|
+
* plugin.ts's PATCH handler) — the "upsert" naming matches ProfileStore's for
|
|
78
|
+
* interface-shape consistency across app-kit's *Store seams, not because this
|
|
79
|
+
* plugin ever creates a flag through it. */
|
|
80
|
+
upsert(key: string, patch: FlagPatch): Promise<FlagRow>;
|
|
81
|
+
}
|
|
82
|
+
export interface AppKitFlagsOptions {
|
|
83
|
+
/** The persistence seam — see FlagStore above. Required. */
|
|
84
|
+
store: FlagStore;
|
|
85
|
+
/**
|
|
86
|
+
* Resolves a flag key to its display metadata, or `undefined` if the key isn't
|
|
87
|
+
* known to the consumer's own catalog (e.g. media-planner's FLAG_ROADMAP). A flag
|
|
88
|
+
* with no metadata is treated as if it doesn't exist through this API at all —
|
|
89
|
+
* excluded from GET {routePrefix} (matching the reference's `.filter((x) =>
|
|
90
|
+
* x.meta)`), and 404s from GET/PATCH {routePrefix}/:key — so every route agrees
|
|
91
|
+
* on the same set of "real" flags rather than list hiding a key that :key would
|
|
92
|
+
* still happily serve. Required.
|
|
93
|
+
*/
|
|
94
|
+
describeFlag: (key: string) => FlagMeta | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* Fired after every successful PATCH, whether or not the patch actually changed
|
|
97
|
+
* anything (mirrors the reference's own unconditional `recordAudit` call, whose
|
|
98
|
+
* `detail` falls back to "No change" rather than skipping the call) — so a
|
|
99
|
+
* consumer replicating its own audit trail doesn't have to separately guess
|
|
100
|
+
* whether "nothing changed" is worth recording. `changes` contains only the
|
|
101
|
+
* fields that differed from the row's previous value, matching what was actually
|
|
102
|
+
* written. Not baked into a specific audit/trace mechanism (no dependency on
|
|
103
|
+
* @flytedesk/app-kit/trace or any other) — the consumer wires this to whatever
|
|
104
|
+
* they already use, same boundary `/profile` never needed to draw because it had
|
|
105
|
+
* no audit trail to begin with.
|
|
106
|
+
*/
|
|
107
|
+
onChange?: (key: string, changes: FlagPatch, actor: {
|
|
108
|
+
id: string;
|
|
109
|
+
name: string;
|
|
110
|
+
}) => void | Promise<void>;
|
|
111
|
+
/** Base path the GET/PATCH routes are mounted under. Default "/flags". */
|
|
112
|
+
routePrefix?: string;
|
|
113
|
+
}
|
|
114
|
+
/** What GET `{routePrefix}` returns for one flag, GET `{routePrefix}/:key`, and
|
|
115
|
+
* PATCH `{routePrefix}/:key` all share. */
|
|
116
|
+
export interface FlagResponseBody {
|
|
117
|
+
key: string;
|
|
118
|
+
project: string;
|
|
119
|
+
label: string;
|
|
120
|
+
description: string;
|
|
121
|
+
enabled: boolean;
|
|
122
|
+
rollout: number;
|
|
123
|
+
roles: string[];
|
|
124
|
+
users: Record<string, boolean>;
|
|
125
|
+
owner: string;
|
|
126
|
+
/** ISO 8601. */
|
|
127
|
+
updated: string;
|
|
128
|
+
}
|
|
129
|
+
export interface FlagPager {
|
|
130
|
+
page: number;
|
|
131
|
+
pageSize: number;
|
|
132
|
+
total: number;
|
|
133
|
+
totalPages: number;
|
|
134
|
+
from: number;
|
|
135
|
+
to: number;
|
|
136
|
+
hasPrev: boolean;
|
|
137
|
+
hasNext: boolean;
|
|
138
|
+
}
|
|
139
|
+
/** What GET `{routePrefix}` returns — search/filter/sort/pagination/facets, same
|
|
140
|
+
* shape as the reference `GET /flags`. */
|
|
141
|
+
export interface FlagListResponseBody {
|
|
142
|
+
rows: FlagResponseBody[];
|
|
143
|
+
pager: FlagPager;
|
|
144
|
+
sort: {
|
|
145
|
+
key: string;
|
|
146
|
+
dir: "asc" | "desc";
|
|
147
|
+
};
|
|
148
|
+
filter: {
|
|
149
|
+
applied: {
|
|
150
|
+
project: string[];
|
|
151
|
+
state: string[];
|
|
152
|
+
};
|
|
153
|
+
q: string;
|
|
154
|
+
facets: {
|
|
155
|
+
project: Record<string, number>;
|
|
156
|
+
state: Record<string, number>;
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-written row/option types for @flytedesk/app-kit/flags.
|
|
3
|
+
*
|
|
4
|
+
* Field names here mirror prisma/fragments/flags.prisma's camelCase model fields
|
|
5
|
+
* (key/project/enabled/rollout/roles/users/updatedAt) — a consumer's generated
|
|
6
|
+
* PrismaClient, once they've run `flags-sync init` and their own `prisma generate`,
|
|
7
|
+
* satisfies FlagStore directly via `prisma.appKitFeatureFlag.findMany` /
|
|
8
|
+
* `.findUnique` / `.upsert`.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/flags/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -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"}
|