@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.
Files changed (37) hide show
  1. package/dist/cli/flags-sync.d.ts +2 -0
  2. package/dist/cli/flags-sync.js +26 -0
  3. package/dist/cli/flags-sync.js.map +1 -0
  4. package/dist/cli/profile-sync.d.ts +2 -0
  5. package/dist/cli/profile-sync.js +26 -0
  6. package/dist/cli/profile-sync.js.map +1 -0
  7. package/dist/cli/sync-engine.d.ts +20 -0
  8. package/dist/cli/sync-engine.js +543 -0
  9. package/dist/cli/sync-engine.js.map +1 -0
  10. package/dist/cli/trace-sync.js +9 -359
  11. package/dist/cli/trace-sync.js.map +1 -1
  12. package/dist/flags/index.d.ts +49 -0
  13. package/dist/flags/index.js +49 -0
  14. package/dist/flags/index.js.map +1 -0
  15. package/dist/flags/plugin.d.ts +3 -0
  16. package/dist/flags/plugin.js +296 -0
  17. package/dist/flags/plugin.js.map +1 -0
  18. package/dist/flags/types.d.ts +159 -0
  19. package/dist/flags/types.js +11 -0
  20. package/dist/flags/types.js.map +1 -0
  21. package/dist/profile/index.d.ts +36 -0
  22. package/dist/profile/index.js +36 -0
  23. package/dist/profile/index.js.map +1 -0
  24. package/dist/profile/plugin.d.ts +3 -0
  25. package/dist/profile/plugin.js +111 -0
  26. package/dist/profile/plugin.js.map +1 -0
  27. package/dist/profile/types.d.ts +76 -0
  28. package/dist/profile/types.js +10 -0
  29. package/dist/profile/types.js.map +1 -0
  30. package/package.json +12 -2
  31. package/prisma/fragments/flags.meta.json +4 -0
  32. package/prisma/fragments/flags.prisma +52 -0
  33. package/prisma/fragments/profile.meta.json +4 -0
  34. package/prisma/fragments/profile.prisma +42 -0
  35. package/prisma/migrations/0002_app_kit_profile_init/migration.sql +15 -0
  36. package/prisma/migrations/0003_app_kit_flags_init/migration.sql +16 -0
  37. package/prisma/migrations/manifest.json +15 -0
@@ -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.1.3",
3
+ "version": "0.3.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,9 @@
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",
33
+ "flags-sync": "./dist/cli/flags-sync.js"
32
34
  },
33
35
  "exports": {
34
36
  "./auth": {
@@ -42,6 +44,14 @@
42
44
  "./rate-limit": {
43
45
  "types": "./dist/rate-limit/index.d.ts",
44
46
  "import": "./dist/rate-limit/index.js"
47
+ },
48
+ "./profile": {
49
+ "types": "./dist/profile/index.d.ts",
50
+ "import": "./dist/profile/index.js"
51
+ },
52
+ "./flags": {
53
+ "types": "./dist/flags/index.d.ts",
54
+ "import": "./dist/flags/index.js"
45
55
  }
46
56
  },
47
57
  "scripts": {
@@ -0,0 +1,4 @@
1
+ {
2
+ "fragmentVersion": "1.0.0",
3
+ "minCliVersion": "1.0.0"
4
+ }
@@ -0,0 +1,52 @@
1
+ // @flytedesk/app-kit/flags — canonical Prisma model fragment (DEC-39, ported to a
2
+ // third fragment for the feature-flags module).
3
+ //
4
+ // This fragment is NOT built into a Prisma Client by this package. It is installed
5
+ // into the *consumer's* own Prisma schema by `flags-sync init` (see
6
+ // src/cli/flags-sync.ts, a thin wrapper around the generalized
7
+ // src/cli/sync-engine.ts, same as trace-sync/profile-sync), and the consumer
8
+ // generates their own client against their own schema + this fragment merged in.
9
+ //
10
+ // What this models: a generic project-scoped feature flag — enabled/disabled, a
11
+ // percentage rollout, role-based targeting, and a per-user override bag. Ported
12
+ // from media-planner's own `FeatureFlag` model (apps/api/prisma/schema.prisma) and
13
+ // its `GET/PATCH /flags` routes (apps/api/src/routes/flags.ts), generalizing the one
14
+ // genuinely app-specific piece: media-planner resolves flag metadata (label,
15
+ // description, owner, project) from its own hardcoded `FLAG_ROADMAP` — this
16
+ // fragment's row has no such columns at all, and @flytedesk/app-kit/flags's plugin
17
+ // (see src/flags/plugin.ts) instead takes a required `describeFlag` callback so any
18
+ // consumer can supply its own metadata source.
19
+ //
20
+ // Design constraints (deliberate, same reasoning as trace.prisma/profile.prisma):
21
+ // - No `@relation`/FK attributes anywhere. A package that ships models into a
22
+ // consumer's schema cannot see the consumer's own models to point a relation at.
23
+ // - No Prisma `enum`. Nothing here needs one today, but the same schema-global-
24
+ // collision reasoning applies if one ever would: it stays a plain `String`/`Int`
25
+ // column instead.
26
+ // - `String @id` on `key`: a flag's key is already a natural, stable, unique
27
+ // identifier (matches media-planner's own `FeatureFlag.key String @id`) — no
28
+ // separate surrogate key earns its keep here.
29
+ // - `roles String[]` and `users Json`: mirrors media-planner's own columns
30
+ // exactly (role ids are opaque strings from whatever IdP/RBAC system the
31
+ // consumer uses — this package has no opinion on what they mean; `users` is a
32
+ // free-form per-user override bag, `Json` because its shape is consumer-defined).
33
+ //
34
+ // Table name is prefixed `app_kit_` via `@@map` so it can't collide with a
35
+ // consumer's own tables (media-planner's own table is unprefixed `FeatureFlag` —
36
+ // this fragment intentionally does NOT reuse that name, since a consumer migrating
37
+ // onto this fragment keeps their own table and migrates data across on their own
38
+ // schedule, same as `/profile` did for flytedesk-id). Column names are snake_case
39
+ // via `@map`, matching the raw SQL in
40
+ // prisma/migrations/0003_app_kit_flags_init/migration.sql exactly.
41
+
42
+ model AppKitFeatureFlag {
43
+ key String @id
44
+ project String
45
+ enabled Boolean @default(true)
46
+ rollout Int @default(100)
47
+ roles String[] @default([])
48
+ users Json @default("{}")
49
+ updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
50
+
51
+ @@map("app_kit_feature_flag")
52
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "fragmentVersion": "1.0.0",
3
+ "minCliVersion": "1.0.0"
4
+ }
@@ -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
+ );
@@ -0,0 +1,16 @@
1
+ -- @flytedesk/app-kit/flags :: 0003_app_kit_flags_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). Column names, types, and defaults match prisma/fragments/flags.prisma
6
+ -- exactly: TEXT for the String @id/String columns, BOOLEAN/INTEGER for their Prisma
7
+ -- counterparts, TEXT[] for String[], JSONB for Json, TIMESTAMPTZ for DateTime.
8
+ CREATE TABLE app_kit_feature_flag (
9
+ key TEXT PRIMARY KEY,
10
+ project TEXT NOT NULL,
11
+ enabled BOOLEAN NOT NULL DEFAULT true,
12
+ rollout INTEGER NOT NULL DEFAULT 100,
13
+ roles TEXT[] NOT NULL DEFAULT '{}',
14
+ users JSONB NOT NULL DEFAULT '{}',
15
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
16
+ );
@@ -1,8 +1,23 @@
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"
15
+ },
16
+ {
17
+ "id": "0003_app_kit_flags_init",
18
+ "fragment": "flags",
19
+ "file": "0003_app_kit_flags_init/migration.sql",
20
+ "checksum": "sha256:55a781c3118fdaf7c4aa9405a8867cecfd8c4b9de0a6e14ef90fc364c63f7d39",
21
+ "introducedInFragmentVersion": "1.0.0"
7
22
  }
8
23
  ]