@flytedesk/app-kit 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/flags-sync.d.ts +2 -0
- package/dist/cli/flags-sync.js +23 -0
- package/dist/cli/flags-sync.js.map +1 -0
- package/dist/cli/profile-sync.js +2 -5
- package/dist/cli/profile-sync.js.map +1 -1
- package/dist/cli/sync-engine.d.ts +32 -0
- package/dist/cli/sync-engine.js +362 -44
- package/dist/cli/sync-engine.js.map +1 -1
- package/dist/cli/trace-sync.js +2 -5
- 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/package.json +7 -2
- package/prisma/fragments/flags.meta.json +4 -0
- package/prisma/fragments/flags.prisma +52 -0
- package/prisma/migrations/0003_app_kit_flags_init/migration.sql +16 -0
- package/prisma/migrations/manifest.json +7 -0
|
@@ -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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flytedesk/app-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"bin": {
|
|
31
31
|
"trace-sync": "./dist/cli/trace-sync.js",
|
|
32
|
-
"profile-sync": "./dist/cli/profile-sync.js"
|
|
32
|
+
"profile-sync": "./dist/cli/profile-sync.js",
|
|
33
|
+
"flags-sync": "./dist/cli/flags-sync.js"
|
|
33
34
|
},
|
|
34
35
|
"exports": {
|
|
35
36
|
"./auth": {
|
|
@@ -47,6 +48,10 @@
|
|
|
47
48
|
"./profile": {
|
|
48
49
|
"types": "./dist/profile/index.d.ts",
|
|
49
50
|
"import": "./dist/profile/index.js"
|
|
51
|
+
},
|
|
52
|
+
"./flags": {
|
|
53
|
+
"types": "./dist/flags/index.d.ts",
|
|
54
|
+
"import": "./dist/flags/index.js"
|
|
50
55
|
}
|
|
51
56
|
},
|
|
52
57
|
"scripts": {
|
|
@@ -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,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
|
+
);
|
|
@@ -12,5 +12,12 @@
|
|
|
12
12
|
"file": "0002_app_kit_profile_init/migration.sql",
|
|
13
13
|
"checksum": "sha256:62d688a2db89c77f12c61a3c98be5cfe84a6e06f8a4dda9d24b0f9acd5511b32",
|
|
14
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"
|
|
15
22
|
}
|
|
16
23
|
]
|