@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,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.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,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,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
|
]
|