@pikku/kysely 0.13.24 → 0.13.28

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 (80) hide show
  1. package/CHANGELOG.md +271 -0
  2. package/dist/src/index.d.ts +4 -1
  3. package/dist/src/index.js +3 -1
  4. package/dist/src/kysely-analytics-service.d.ts +30 -0
  5. package/dist/src/kysely-analytics-service.js +80 -0
  6. package/dist/src/kysely-feature-flag-store.d.ts +37 -0
  7. package/dist/src/kysely-feature-flag-store.js +239 -0
  8. package/dist/src/kysely-scope-service.d.ts +1 -0
  9. package/dist/src/kysely-scope-service.js +29 -0
  10. package/dist/src/kysely-tables.d.ts +45 -0
  11. package/dist/src/schema/analytics.schema.d.ts +17 -0
  12. package/dist/src/schema/analytics.schema.js +48 -0
  13. package/dist/src/schema/flag.schema.d.ts +16 -0
  14. package/dist/src/schema/flag.schema.js +62 -0
  15. package/dist/src/schema/index.d.ts +2 -0
  16. package/dist/src/schema/index.js +6 -0
  17. package/package.json +11 -6
  18. package/run-tests.sh +9 -1
  19. package/bin/pikku-kysely-pure.ts +0 -59
  20. package/dist/src/kysely-lock-vault.d.ts +0 -34
  21. package/dist/src/kysely-lock-vault.js +0 -77
  22. package/dist/src/schema/data-lock.schema.d.ts +0 -15
  23. package/dist/src/schema/data-lock.schema.js +0 -27
  24. package/dist/tsconfig.tsbuildinfo +0 -1
  25. package/src/classification-crypto.test.ts +0 -143
  26. package/src/classification-crypto.ts +0 -163
  27. package/src/classification-plugin.pglite.test.ts +0 -226
  28. package/src/classification-plugin.test.ts +0 -265
  29. package/src/classification-plugin.ts +0 -249
  30. package/src/coercion-plugin.test.ts +0 -160
  31. package/src/coercion-plugin.ts +0 -187
  32. package/src/create-audited-kysely.test.ts +0 -560
  33. package/src/create-audited-kysely.ts +0 -254
  34. package/src/index.ts +0 -81
  35. package/src/kysely-agent-run-scores.ts +0 -47
  36. package/src/kysely-agent-run-service.ts +0 -244
  37. package/src/kysely-agent-run-state-service.ts +0 -200
  38. package/src/kysely-agent-storage-service.ts +0 -632
  39. package/src/kysely-audit-service.test.ts +0 -352
  40. package/src/kysely-audit-service.ts +0 -237
  41. package/src/kysely-channel-store.ts +0 -107
  42. package/src/kysely-credential-service.ts +0 -286
  43. package/src/kysely-deployment-service.ts +0 -163
  44. package/src/kysely-eventhub-store.ts +0 -53
  45. package/src/kysely-json.ts +0 -5
  46. package/src/kysely-scope-service.test.ts +0 -418
  47. package/src/kysely-scope-service.ts +0 -425
  48. package/src/kysely-secret-service.ts +0 -242
  49. package/src/kysely-services.test.ts +0 -405
  50. package/src/kysely-session-store.ts +0 -57
  51. package/src/kysely-tables.test.ts +0 -110
  52. package/src/kysely-tables.ts +0 -434
  53. package/src/kysely-virtual-user-run-store.test.ts +0 -369
  54. package/src/kysely-virtual-user-run-store.ts +0 -235
  55. package/src/kysely-virtual-user-schedule-store.test.ts +0 -187
  56. package/src/kysely-virtual-user-schedule-store.ts +0 -187
  57. package/src/kysely-webhook-service.test.ts +0 -153
  58. package/src/kysely-webhook-service.ts +0 -139
  59. package/src/kysely-workflow-mirror.test.ts +0 -259
  60. package/src/kysely-workflow-mirror.ts +0 -374
  61. package/src/kysely-workflow-run-service.ts +0 -305
  62. package/src/kysely-workflow-service.test.ts +0 -544
  63. package/src/kysely-workflow-service.ts +0 -820
  64. package/src/schema/agent.schema.ts +0 -162
  65. package/src/schema/audit.schema.ts +0 -57
  66. package/src/schema/channel.schema.ts +0 -38
  67. package/src/schema/credential.schema.ts +0 -58
  68. package/src/schema/deployment.schema.ts +0 -47
  69. package/src/schema/index.ts +0 -441
  70. package/src/schema/pikku-schema.types.ts +0 -169
  71. package/src/schema/schema.test.ts +0 -591
  72. package/src/schema/scope.schema.ts +0 -110
  73. package/src/schema/secret.schema.ts +0 -49
  74. package/src/schema/session.schema.ts +0 -20
  75. package/src/schema/virtual-user-schedule.schema.ts +0 -48
  76. package/src/schema/virtual-user.schema.ts +0 -106
  77. package/src/schema/webhook.schema.ts +0 -58
  78. package/src/schema/workflow.schema.ts +0 -150
  79. package/src/serialize-plugin.ts +0 -233
  80. package/tsconfig.json +0 -17
@@ -0,0 +1,239 @@
1
+ import { CachedFlagSource, subjectIdOf } from '@pikku/core/flag';
2
+ import { requirePikkuSchema } from './schema/index.js';
3
+ import { flagSchema } from './schema/flag.schema.js';
4
+ /**
5
+ * Reads the stored `anyOf`, which arrives as either shape.
6
+ *
7
+ * The column holds JSON text, but `SerializePlugin` parses it back on the way
8
+ * out of a sqlite-style dialect while postgres hands over the string as
9
+ * written. One store serving both engines has to accept both, and the column
10
+ * type can only describe one of them.
11
+ */
12
+ const parseAnyOf = (value) => {
13
+ if (!value)
14
+ return undefined;
15
+ const parsed = typeof value === 'string'
16
+ ? (() => {
17
+ try {
18
+ return JSON.parse(value);
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ })()
24
+ : value;
25
+ return Array.isArray(parsed)
26
+ ? parsed.filter((v) => typeof v === 'string')
27
+ : undefined;
28
+ };
29
+ /**
30
+ * Feature flags backed by two tables pikku owns.
31
+ *
32
+ * The full store, not just a source: the declaration lifecycle and the operator
33
+ * controls both live here, which is what makes the console's Flags tab
34
+ * writable. A third-party provider implements `FeatureFlagSource` alone.
35
+ */
36
+ export class KyselyFeatureFlagStore extends CachedFlagSource {
37
+ db;
38
+ initialized = false;
39
+ constructor(db, options = {}) {
40
+ super({ ttlMs: 10_000, ...options });
41
+ this.db = db;
42
+ }
43
+ async init() {
44
+ if (this.initialized)
45
+ return;
46
+ await requirePikkuSchema(this.db, flagSchema);
47
+ this.initialized = true;
48
+ }
49
+ async fetchSnapshot() {
50
+ const [flags, overrides] = await Promise.all([
51
+ this.db
52
+ .selectFrom('pikkuFeatureFlags')
53
+ .select(['name', 'enabled', 'rolloutPercent'])
54
+ .execute(),
55
+ this.db
56
+ .selectFrom('pikkuFeatureFlagOverrides')
57
+ .select(['flag', 'subjectId', 'enabled'])
58
+ .execute(),
59
+ ]);
60
+ const snapshot = {};
61
+ for (const flag of flags) {
62
+ snapshot[flag.name] = {
63
+ enabled: !!flag.enabled,
64
+ rolloutPercent: flag.rolloutPercent ?? null,
65
+ overrides: {},
66
+ };
67
+ }
68
+ for (const override of overrides) {
69
+ const row = snapshot[override.flag];
70
+ if (row) {
71
+ row.overrides[override.subjectId] = !!override.enabled;
72
+ }
73
+ }
74
+ return snapshot;
75
+ }
76
+ async syncFlags(flags) {
77
+ this.setDeclared(flags);
78
+ await this.db.transaction().execute(async (trx) => {
79
+ for (const flag of flags) {
80
+ await trx
81
+ .insertInto('pikkuFeatureFlags')
82
+ .values({
83
+ name: flag.name,
84
+ description: flag.description ?? null,
85
+ anyOf: flag.anyOf ? JSON.stringify(flag.anyOf) : null,
86
+ declared: true,
87
+ })
88
+ .onConflict((oc) =>
89
+ // `enabled`, `rolloutPercent` and every override are the operator's
90
+ // and are never touched here. A deploy that re-enabled a flag
91
+ // somebody killed an hour earlier would make the kill switch
92
+ // useless exactly when it is being relied on.
93
+ oc.column('name').doUpdateSet((eb) => ({
94
+ description: eb.ref('excluded.description'),
95
+ anyOf: eb.ref('excluded.anyOf'),
96
+ declared: true,
97
+ })))
98
+ .execute();
99
+ }
100
+ const markStale = trx
101
+ .updateTable('pikkuFeatureFlags')
102
+ .set({ declared: false });
103
+ await (flags.length > 0
104
+ ? markStale.where('name', 'not in', flags.map((f) => f.name))
105
+ : markStale).execute();
106
+ });
107
+ this.invalidate();
108
+ }
109
+ async listFlags() {
110
+ const rows = await this.db
111
+ .selectFrom('pikkuFeatureFlags')
112
+ .selectAll()
113
+ .orderBy('name')
114
+ .execute();
115
+ return rows.map((row) => ({
116
+ name: row.name,
117
+ description: row.description ?? undefined,
118
+ anyOf: parseAnyOf(row.anyOf),
119
+ enabled: !!row.enabled,
120
+ rolloutPercent: row.rolloutPercent ?? null,
121
+ declared: !!row.declared,
122
+ }));
123
+ }
124
+ async listOverrides(key) {
125
+ const rows = await this.db
126
+ .selectFrom('pikkuFeatureFlagOverrides')
127
+ .selectAll()
128
+ .where('flag', '=', key)
129
+ .orderBy('subjectId')
130
+ .execute();
131
+ return rows.map((row) => ({
132
+ subjectId: row.subjectId,
133
+ subjectKind: row.subjectKind,
134
+ enabled: !!row.enabled,
135
+ grantedBy: row.grantedBy ?? undefined,
136
+ // ISO 8601 rather than the driver's date: the row crosses an RPC boundary
137
+ // to reach the console, and every driver spells a timestamp differently.
138
+ grantedAt: row.grantedAt
139
+ ? new Date(row.grantedAt).toISOString()
140
+ : undefined,
141
+ }));
142
+ }
143
+ async setEnabled(key, enabled, actor, note) {
144
+ await this.db
145
+ .updateTable('pikkuFeatureFlags')
146
+ .set({
147
+ enabled,
148
+ updatedBy: actor ?? null,
149
+ note: note ?? null,
150
+ updatedAt: new Date(),
151
+ })
152
+ .where('name', '=', key)
153
+ .execute();
154
+ this.invalidate();
155
+ }
156
+ async setRollout(key, percent, actor) {
157
+ if (percent !== null && (percent < 0 || percent > 100)) {
158
+ throw new Error(`Feature flag '${key}': rollout must be between 0 and 100, got ${percent}.`);
159
+ }
160
+ await this.db
161
+ .updateTable('pikkuFeatureFlags')
162
+ .set({
163
+ rolloutPercent: percent === null ? null : Math.round(percent),
164
+ updatedBy: actor ?? null,
165
+ updatedAt: new Date(),
166
+ })
167
+ .where('name', '=', key)
168
+ .execute();
169
+ this.invalidate();
170
+ }
171
+ async setOverride(key, subject, enabled, actor) {
172
+ const subjectId = subjectIdOf(subject);
173
+ if (!subjectId) {
174
+ throw new Error(`Feature flag '${key}': an override needs an organization or a user to be about.`);
175
+ }
176
+ await this.db
177
+ .insertInto('pikkuFeatureFlagOverrides')
178
+ .values({
179
+ flag: key,
180
+ subjectId,
181
+ subjectKind: subject.organizationId ? 'organization' : 'user',
182
+ enabled,
183
+ grantedBy: actor ?? null,
184
+ grantedAt: new Date(),
185
+ })
186
+ .onConflict((oc) => oc.columns(['flag', 'subjectId']).doUpdateSet((eb) => ({
187
+ enabled: eb.ref('excluded.enabled'),
188
+ subjectKind: eb.ref('excluded.subjectKind'),
189
+ grantedBy: eb.ref('excluded.grantedBy'),
190
+ // The column defaults on insert only, and the panel reads this as
191
+ // when the pin was last granted — not when the subject was first
192
+ // pinned to something else.
193
+ grantedAt: eb.ref('excluded.grantedAt'),
194
+ })))
195
+ .execute();
196
+ this.invalidate();
197
+ }
198
+ async clearOverride(key, subject) {
199
+ const subjectId = subjectIdOf(subject);
200
+ if (!subjectId) {
201
+ return;
202
+ }
203
+ await this.db
204
+ .deleteFrom('pikkuFeatureFlagOverrides')
205
+ .where('flag', '=', key)
206
+ .where('subjectId', '=', subjectId)
207
+ .execute();
208
+ this.invalidate();
209
+ }
210
+ async findStaleFlags() {
211
+ const rows = await this.db
212
+ .selectFrom('pikkuFeatureFlags')
213
+ .select('name')
214
+ .where('declared', '=', false)
215
+ .orderBy('name')
216
+ .execute();
217
+ return rows.map((row) => row.name);
218
+ }
219
+ /**
220
+ * Deletes what is still undeclared, and reports only what it deleted.
221
+ *
222
+ * The `declared = false` predicate is repeated on the delete rather than
223
+ * carried over from the read: a deploy running `syncFlags` between the two
224
+ * would redeclare a flag, and deleting on the earlier answer would take a
225
+ * live flag and cascade away every override an operator had set on it.
226
+ */
227
+ async pruneFlags() {
228
+ const deleted = await this.db
229
+ .deleteFrom('pikkuFeatureFlags')
230
+ .where('declared', '=', false)
231
+ .returning('name')
232
+ .execute();
233
+ if (deleted.length === 0) {
234
+ return [];
235
+ }
236
+ this.invalidate();
237
+ return deleted.map((row) => row.name).sort();
238
+ }
239
+ }
@@ -61,6 +61,7 @@ export declare class KyselyScopeService implements ScopeService {
61
61
  addUserToRole(userId: string, role: string, grantedBy?: string): Promise<void>;
62
62
  removeUserFromRole(userId: string, role: string): Promise<void>;
63
63
  listUserRoles(userId: string): Promise<string[]>;
64
+ listRolesForUsers(userIds: string[]): Promise<Record<string, string[]>>;
64
65
  addScopeToUser(userId: string, scope: string, grantedBy?: string): Promise<void>;
65
66
  removeScopeFromUser(userId: string, scope: string): Promise<void>;
66
67
  listUserScopes(userId: string): Promise<string[]>;
@@ -236,6 +236,35 @@ export class KyselyScopeService {
236
236
  .execute();
237
237
  return rows.map((row) => row.role);
238
238
  }
239
+ async listRolesForUsers(userIds) {
240
+ // Seeded with every id asked for, so a user holding no roles is an empty
241
+ // array rather than a missing key.
242
+ const byUser = {};
243
+ for (const userId of userIds) {
244
+ byUser[userId] = [];
245
+ }
246
+ const distinct = Object.keys(byUser);
247
+ if (distinct.length === 0) {
248
+ return byUser;
249
+ }
250
+ // Chunked because an `in` list is one bound parameter per id, and every
251
+ // engine caps those — SQLite at 999 on older builds, Postgres at 65535 for
252
+ // the whole statement. A caller asking for a page of users stays well
253
+ // inside one chunk; one asking for thousands still gets an answer instead
254
+ // of a driver error.
255
+ for (let i = 0; i < distinct.length; i += 500) {
256
+ const chunk = distinct.slice(i, i + 500);
257
+ const rows = await this.db
258
+ .selectFrom('pikkuUserRole')
259
+ .select(['userId', 'role'])
260
+ .where('userId', 'in', chunk)
261
+ .execute();
262
+ for (const row of rows) {
263
+ byUser[row.userId].push(row.role);
264
+ }
265
+ }
266
+ return byUser;
267
+ }
239
268
  async addScopeToUser(userId, scope, grantedBy) {
240
269
  await this.db
241
270
  .insertInto('pikkuUserScope')
@@ -222,6 +222,29 @@ export interface PikkuRolesTable {
222
222
  declared: Generated<boolean>;
223
223
  createdAt: Generated<Date>;
224
224
  }
225
+ /** A feature flag's operator state. What code declared lives beside it. */
226
+ export interface PikkuFeatureFlagsTable {
227
+ name: string;
228
+ description: string | null;
229
+ /** The declared `anyOf` scope list as JSON, or null for a pure switch. */
230
+ anyOf: string | null;
231
+ enabled: Generated<boolean>;
232
+ rolloutPercent: number | null;
233
+ /** False for a flag whose declaration has gone: switchable, but not offered. */
234
+ declared: Generated<boolean>;
235
+ updatedBy: string | null;
236
+ note: string | null;
237
+ updatedAt: Generated<Date>;
238
+ }
239
+ /** One subject's short-circuit, in either direction, over a flag's rollout. */
240
+ export interface PikkuFeatureFlagOverridesTable {
241
+ flag: string;
242
+ subjectId: string;
243
+ subjectKind: string;
244
+ enabled: boolean;
245
+ grantedBy: string | null;
246
+ grantedAt: Generated<Date>;
247
+ }
225
248
  export interface PikkuRoleScopesTable {
226
249
  role: string;
227
250
  scope: string;
@@ -317,6 +340,25 @@ export interface AuditTable {
317
340
  old: string | null;
318
341
  data: string | null;
319
342
  }
343
+ /** One analytics event, as {@link KyselyAnalyticsService} appended it. */
344
+ export interface PikkuAnalyticsEventsTable {
345
+ eventId: string;
346
+ name: string;
347
+ /** ISO 8601, because string ordering is chronological ordering. */
348
+ occurredAt: string;
349
+ source: string;
350
+ functionId: string | null;
351
+ wireType: string | null;
352
+ traceId: string | null;
353
+ userId: string | null;
354
+ orgId: string | null;
355
+ pikkuUserId: string | null;
356
+ /** The device id for a visitor with no session yet. */
357
+ anonymousId: string | null;
358
+ vendorIds: string | null;
359
+ consent: string | null;
360
+ props: string | null;
361
+ }
320
362
  /**
321
363
  * One turn of a run's transcript.
322
364
  *
@@ -366,6 +408,8 @@ export interface KyselyPikkuDB {
366
408
  pikkuRoleScopes: PikkuRoleScopesTable;
367
409
  pikkuUserRole: PikkuUserRoleTable;
368
410
  pikkuUserScope: PikkuUserScopeTable;
411
+ pikkuFeatureFlags: PikkuFeatureFlagsTable;
412
+ pikkuFeatureFlagOverrides: PikkuFeatureFlagOverridesTable;
369
413
  channels: ChannelsTable;
370
414
  channelSubscriptions: ChannelSubscriptionsTable;
371
415
  workflowRuns: WorkflowRunsTable;
@@ -393,4 +437,5 @@ export interface KyselyPikkuDB {
393
437
  virtualUserRunStep: VirtualUserRunStepTable;
394
438
  virtualUserSchedule: VirtualUserScheduleTable;
395
439
  audit: AuditTable;
440
+ pikkuAnalyticsEvents: PikkuAnalyticsEventsTable;
396
441
  }
@@ -0,0 +1,17 @@
1
+ import type { PikkuSchema } from './pikku-schema.types.js';
2
+ /**
3
+ * The `pikkuAnalyticsEvents` table {@link KyselyAnalyticsService} appends to.
4
+ *
5
+ * Gated by `ownedBy` so it arrives only for a project that declares events: a
6
+ * database that measures nothing should not carry a table for it.
7
+ *
8
+ * Every column is text, and `occurredAt` is an ISO 8601 string rather than a
9
+ * timestamp, for the same reason the audit trail is: a warehouse loader reading
10
+ * these rows has no shared type to agree on, and string ordering of ISO 8601 is
11
+ * chronological ordering.
12
+ *
13
+ * The identity is spread across columns rather than left inside the JSON
14
+ * because it is what every query groups by — one visitor's funnel, one org's
15
+ * usage — while `props` differs per event and is only ever read back whole.
16
+ */
17
+ export declare const analyticsSchema: PikkuSchema;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The `pikkuAnalyticsEvents` table {@link KyselyAnalyticsService} appends to.
3
+ *
4
+ * Gated by `ownedBy` so it arrives only for a project that declares events: a
5
+ * database that measures nothing should not carry a table for it.
6
+ *
7
+ * Every column is text, and `occurredAt` is an ISO 8601 string rather than a
8
+ * timestamp, for the same reason the audit trail is: a warehouse loader reading
9
+ * these rows has no shared type to agree on, and string ordering of ISO 8601 is
10
+ * chronological ordering.
11
+ *
12
+ * The identity is spread across columns rather than left inside the JSON
13
+ * because it is what every query groups by — one visitor's funnel, one org's
14
+ * usage — while `props` differs per event and is only ever read back whole.
15
+ */
16
+ export const analyticsSchema = {
17
+ name: 'analytics',
18
+ ownedBy: ['analyticsService'],
19
+ statements: [
20
+ (db) => db.schema
21
+ .createTable('pikkuAnalyticsEvents')
22
+ .addColumn('eventId', 'text', (col) => col.primaryKey())
23
+ .addColumn('name', 'text', (col) => col.notNull())
24
+ .addColumn('occurredAt', 'text', (col) => col.notNull())
25
+ .addColumn('source', 'text', (col) => col.notNull())
26
+ .addColumn('functionId', 'text')
27
+ .addColumn('wireType', 'text')
28
+ .addColumn('traceId', 'text')
29
+ .addColumn('userId', 'text')
30
+ .addColumn('orgId', 'text')
31
+ .addColumn('pikkuUserId', 'text')
32
+ // The device id for a visitor with no session. Its own column beside
33
+ // `userId` and not folded into it: the whole point of the anonymous id
34
+ // is the traffic that has no user to be attributed to yet.
35
+ .addColumn('anonymousId', 'text')
36
+ .addColumn('vendorIds', 'text')
37
+ .addColumn('consent', 'text')
38
+ .addColumn('props', 'text'),
39
+ (db) => db.schema
40
+ .createIndex('idx_pikku_analytics_events_occurred_at')
41
+ .on('pikkuAnalyticsEvents')
42
+ .column('occurredAt'),
43
+ (db) => db.schema
44
+ .createIndex('idx_pikku_analytics_events_name')
45
+ .on('pikkuAnalyticsEvents')
46
+ .column('name'),
47
+ ],
48
+ };
@@ -0,0 +1,16 @@
1
+ import type { PikkuSchema } from './pikku-schema.types.js';
2
+ /**
3
+ * Feature flags and their per-subject overrides.
4
+ *
5
+ * Two tables and not one because the two halves have different cardinality: a
6
+ * flag is one row an operator flips, an override is an unbounded set keyed by
7
+ * organization or user. Folding the overrides into a JSON column on the flag
8
+ * would make granting one tenant early access a read-modify-write of a blob two
9
+ * consoles can race on.
10
+ *
11
+ * Nothing here references `user` or any organization table. A subject id is
12
+ * stored as opaque text on purpose: pikku does not own the organization
13
+ * concept, and a foreign key would make flags require a table shape that only
14
+ * some projects have.
15
+ */
16
+ export declare const flagSchema: PikkuSchema;
@@ -0,0 +1,62 @@
1
+ import { sql } from 'kysely';
2
+ /**
3
+ * Feature flags and their per-subject overrides.
4
+ *
5
+ * Two tables and not one because the two halves have different cardinality: a
6
+ * flag is one row an operator flips, an override is an unbounded set keyed by
7
+ * organization or user. Folding the overrides into a JSON column on the flag
8
+ * would make granting one tenant early access a read-modify-write of a blob two
9
+ * consoles can race on.
10
+ *
11
+ * Nothing here references `user` or any organization table. A subject id is
12
+ * stored as opaque text on purpose: pikku does not own the organization
13
+ * concept, and a foreign key would make flags require a table shape that only
14
+ * some projects have.
15
+ */
16
+ export const flagSchema = {
17
+ name: 'flag',
18
+ ownedBy: ['featureFlags'],
19
+ statements: [
20
+ (db) => db.schema
21
+ .createTable('pikkuFeatureFlags')
22
+ .addColumn('name', 'text', (col) => col.primaryKey())
23
+ .addColumn('description', 'text')
24
+ // The declared `anyOf` scope list, as JSON. Read by the console to say
25
+ // who would see the feature; never read on the request path, where the
26
+ // capability half comes from the session's own scopes.
27
+ .addColumn('anyOf', 'text')
28
+ // Off until somebody turns it on. A flag exists so a feature can ship
29
+ // dark, so the row a deploy creates has to start closed — the opposite
30
+ // default would make merging the declaration the moment of launch.
31
+ .addColumn('enabled', 'boolean', (col) => col.defaultTo(false).notNull())
32
+ // 0-100, or null for no rollout constraint. Null and 100 are not the
33
+ // same statement: null says the percentage is not part of this flag's
34
+ // story, 100 says a rollout reached everyone and could be wound back.
35
+ .addColumn('rolloutPercent', 'integer')
36
+ // False once the declaration has gone from code: still switchable, no
37
+ // longer offered, awaiting `pikku flags prune`. The same additive
38
+ // contract as `pikkuScopes.declared`.
39
+ .addColumn('declared', 'boolean', (col) => col.defaultTo(true).notNull())
40
+ .addColumn('updatedBy', 'text')
41
+ // Why it was flipped. A kill switch is pulled during an incident, and
42
+ // the one question afterwards is always who turned it off and why.
43
+ .addColumn('note', 'text')
44
+ .addColumn('updatedAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull()),
45
+ (db) => db.schema
46
+ .createTable('pikkuFeatureFlagOverrides')
47
+ .addColumn('flag', 'text', (col) => col.notNull().references('pikkuFeatureFlags.name').onDelete('cascade'))
48
+ .addColumn('subjectId', 'text', (col) => col.notNull())
49
+ // Which id this is, for the console to render. Resolution keys on the
50
+ // id alone, because that is all the resolver has: it prefers the
51
+ // organization and falls back to the user, and by then the two are one
52
+ // string.
53
+ .addColumn('subjectKind', 'text', (col) => col.notNull())
54
+ .addColumn('enabled', 'boolean', (col) => col.notNull())
55
+ .addColumn('grantedBy', 'text')
56
+ .addColumn('grantedAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
57
+ .addPrimaryKeyConstraint('pikku_feature_flag_overrides_pk', [
58
+ 'flag',
59
+ 'subjectId',
60
+ ]),
61
+ ],
62
+ };
@@ -18,10 +18,12 @@ export { rawStatement, requiredType, schemaContext, unqualifiedContext, } from '
18
18
  */
19
19
  export declare const pikkuSchemas: PikkuSchema[];
20
20
  export { agentSchema } from './agent.schema.js';
21
+ export { analyticsSchema } from './analytics.schema.js';
21
22
  export { auditSchema } from './audit.schema.js';
22
23
  export { channelSchema } from './channel.schema.js';
23
24
  export { credentialSchema } from './credential.schema.js';
24
25
  export { deploymentSchema } from './deployment.schema.js';
26
+ export { flagSchema } from './flag.schema.js';
25
27
  export { scopeSchema } from './scope.schema.js';
26
28
  export { secretSchema } from './secret.schema.js';
27
29
  export { sessionSchema } from './session.schema.js';
@@ -1,11 +1,13 @@
1
1
  import { CamelCasePlugin } from 'kysely';
2
2
  import { schemaContext, } from './pikku-schema.types.js';
3
3
  import { agentSchema } from './agent.schema.js';
4
+ import { analyticsSchema } from './analytics.schema.js';
4
5
  import { auditSchema } from './audit.schema.js';
5
6
  import { channelSchema } from './channel.schema.js';
6
7
  import { credentialSchema } from './credential.schema.js';
7
8
  import { deploymentSchema } from './deployment.schema.js';
8
9
  import { scopeSchema } from './scope.schema.js';
10
+ import { flagSchema } from './flag.schema.js';
9
11
  import { secretSchema } from './secret.schema.js';
10
12
  import { sessionSchema } from './session.schema.js';
11
13
  import { webhookSchema } from './webhook.schema.js';
@@ -37,15 +39,19 @@ export const pikkuSchemas = [
37
39
  workflowSchema,
38
40
  agentSchema,
39
41
  scopeSchema,
42
+ flagSchema,
40
43
  auditSchema,
44
+ analyticsSchema,
41
45
  virtualUserSchema,
42
46
  virtualUserScheduleSchema,
43
47
  ];
44
48
  export { agentSchema } from './agent.schema.js';
49
+ export { analyticsSchema } from './analytics.schema.js';
45
50
  export { auditSchema } from './audit.schema.js';
46
51
  export { channelSchema } from './channel.schema.js';
47
52
  export { credentialSchema } from './credential.schema.js';
48
53
  export { deploymentSchema } from './deployment.schema.js';
54
+ export { flagSchema } from './flag.schema.js';
49
55
  export { scopeSchema } from './scope.schema.js';
50
56
  export { secretSchema } from './secret.schema.js';
51
57
  export { sessionSchema } from './session.schema.js';
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@pikku/kysely",
3
- "version": "0.13.24",
3
+ "version": "0.13.28",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/pikkujs/pikku.git",
7
+ "directory": "packages/services/kysely"
8
+ },
4
9
  "description": "Kysely-backed Pikku services — workflow state, secrets, sessions, scopes and audit",
5
10
  "author": "yasser.fadl@gmail.com",
6
11
  "license": "MIT",
@@ -18,23 +23,23 @@
18
23
  "test:watch": "bash run-tests.sh --watch",
19
24
  "test:coverage": "bash run-tests.sh --coverage",
20
25
  "release": "npm run build && npm test",
21
- "prepublishOnly": "yarn build"
26
+ "prepublishOnly": "bun run build"
22
27
  },
23
28
  "peerDependencies": {
24
- "@pikku/core": "^0.12.104"
29
+ "@pikku/core": "^0.12.120"
25
30
  },
26
31
  "dependencies": {
27
32
  "kysely": "^0.29.5"
28
33
  },
29
34
  "devDependencies": {
35
+ "@electric-sql/pglite": "^0.5.5",
30
36
  "@types/better-sqlite3": "^9.6.0",
31
37
  "better-sqlite3": "^13.0.3",
32
38
  "kysely-codegen": "^0.20.0",
33
39
  "kysely-postgres-js": "^3.0.0",
34
- "postgres": "^3.4.9",
35
- "typescript": "^6.0.3"
40
+ "postgres": "^3.4.9"
36
41
  },
37
42
  "exports": {
38
43
  ".": "./dist/src/index.js"
39
44
  }
40
- }
45
+ }
package/run-tests.sh CHANGED
@@ -34,10 +34,17 @@ files=($(find src -type f -name "*.test.ts"))
34
34
  # Check if any files matched the pattern
35
35
  if [ ${#files[@]} -eq 0 ]; then
36
36
  echo "No test files found matching pattern: $pattern"
37
+ if [ "${STRICT_TEST_DISCOVERY}" = "1" ]; then
38
+ exit 1
39
+ fi
37
40
  exit 0
38
41
  fi
39
42
 
40
43
  # Construct the node command
44
+ #
45
+ # Every other package runs its tests on bun. This one stays on node because
46
+ # better-sqlite3, which every one of these suites opens its fixture database
47
+ # with, aborts the bun process on load.
41
48
  node_cmd=(node --import tsx --test)
42
49
 
43
50
  # Append options based on flags
@@ -47,7 +54,8 @@ fi
47
54
 
48
55
  if [ "$coverage_mode" = true ]; then
49
56
  node_cmd+=(--test-coverage-include="src/**/*.{ts,js}" --test-coverage-exclude="**/dist/**" --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info)
57
+ export PIKKU_TEST_COVERAGE=1
50
58
  fi
51
59
 
52
60
  # Execute the node command with the expanded list of files
53
- "${node_cmd[@]}" "${files[@]}"
61
+ "${node_cmd[@]}" "${files[@]}"