@bluefields/db 0.2.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 (76) hide show
  1. package/LICENSE +202 -0
  2. package/dist/client.d.ts +6 -0
  3. package/dist/client.js +13 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/index.d.ts +19 -0
  6. package/dist/index.js +20 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/migrate.d.ts +11 -0
  9. package/dist/migrate.js +48 -0
  10. package/dist/migrate.js.map +1 -0
  11. package/dist/schema/api-keys.d.ts +145 -0
  12. package/dist/schema/api-keys.js +18 -0
  13. package/dist/schema/api-keys.js.map +1 -0
  14. package/dist/schema/audit-log.d.ts +228 -0
  15. package/dist/schema/audit-log.js +49 -0
  16. package/dist/schema/audit-log.js.map +1 -0
  17. package/dist/schema/cost-records.d.ts +190 -0
  18. package/dist/schema/cost-records.js +55 -0
  19. package/dist/schema/cost-records.js.map +1 -0
  20. package/dist/schema/crawl-job-pages.d.ts +186 -0
  21. package/dist/schema/crawl-job-pages.js +31 -0
  22. package/dist/schema/crawl-job-pages.js.map +1 -0
  23. package/dist/schema/crawl-jobs.d.ts +271 -0
  24. package/dist/schema/crawl-jobs.js +40 -0
  25. package/dist/schema/crawl-jobs.js.map +1 -0
  26. package/dist/schema/customers.d.ts +145 -0
  27. package/dist/schema/customers.js +19 -0
  28. package/dist/schema/customers.js.map +1 -0
  29. package/dist/schema/event-feedback.d.ts +170 -0
  30. package/dist/schema/event-feedback.js +35 -0
  31. package/dist/schema/event-feedback.js.map +1 -0
  32. package/dist/schema/events.d.ts +291 -0
  33. package/dist/schema/events.js +58 -0
  34. package/dist/schema/events.js.map +1 -0
  35. package/dist/schema/extractions.d.ts +327 -0
  36. package/dist/schema/extractions.js +56 -0
  37. package/dist/schema/extractions.js.map +1 -0
  38. package/dist/schema/host-extractors.d.ts +131 -0
  39. package/dist/schema/host-extractors.js +18 -0
  40. package/dist/schema/host-extractors.js.map +1 -0
  41. package/dist/schema/host-records.d.ts +237 -0
  42. package/dist/schema/host-records.js +42 -0
  43. package/dist/schema/host-records.js.map +1 -0
  44. package/dist/schema/notification-preferences.d.ts +195 -0
  45. package/dist/schema/notification-preferences.js +49 -0
  46. package/dist/schema/notification-preferences.js.map +1 -0
  47. package/dist/schema/scrape-cache.d.ts +136 -0
  48. package/dist/schema/scrape-cache.js +23 -0
  49. package/dist/schema/scrape-cache.js.map +1 -0
  50. package/dist/schema/sessions.d.ts +153 -0
  51. package/dist/schema/sessions.js +27 -0
  52. package/dist/schema/sessions.js.map +1 -0
  53. package/dist/schema/snapshots.d.ts +382 -0
  54. package/dist/schema/snapshots.js +60 -0
  55. package/dist/schema/snapshots.js.map +1 -0
  56. package/dist/schema/subscriptions.d.ts +604 -0
  57. package/dist/schema/subscriptions.js +79 -0
  58. package/dist/schema/subscriptions.js.map +1 -0
  59. package/dist/schema/usage-periods.d.ts +446 -0
  60. package/dist/schema/usage-periods.js +71 -0
  61. package/dist/schema/usage-periods.js.map +1 -0
  62. package/dist/tsconfig.tsbuildinfo +1 -0
  63. package/package.json +54 -0
  64. package/src/migrations/0001_initial.sql +398 -0
  65. package/src/migrations/0002_pg_cron.sql +109 -0
  66. package/src/migrations/0003_webhook_secret_cleanup.sql +55 -0
  67. package/src/migrations/0004_events_pg_notify.sql +39 -0
  68. package/src/migrations/0005_subscription_consecutive_failures.sql +19 -0
  69. package/src/migrations/0006_notification_preferences.sql +27 -0
  70. package/src/migrations/0007_crawl_jobs.sql +65 -0
  71. package/src/migrations/0008_sessions.sql +38 -0
  72. package/src/migrations/0009_scrape_cache.sql +34 -0
  73. package/src/migrations/0010_neon_defaults.sql +99 -0
  74. package/src/migrations/0011_credits_consumed_numeric.sql +12 -0
  75. package/src/migrations/0012_crawl_page_attestation.sql +10 -0
  76. package/src/migrations/0013_customer_deleted_at.sql +5 -0
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Per-host config table (Review #4; deferred from Review #2).
3
+ *
4
+ * Drives fetcher tier routing, proxy provider preference, politeness limits,
5
+ * robots-policy tracking, and takedown status. Small (one row per host we've
6
+ * ever fetched), read-heavy — cache in memory in the fetcher workers; refresh
7
+ * every 60 seconds.
8
+ */
9
+ export declare const hostRecords: import("drizzle-orm/pg-core").PgTableWithColumns<{
10
+ name: "host_records";
11
+ schema: undefined;
12
+ columns: {
13
+ host: import("drizzle-orm/pg-core").PgColumn<{
14
+ name: "host";
15
+ tableName: "host_records";
16
+ dataType: "string";
17
+ columnType: "PgText";
18
+ data: string;
19
+ driverParam: string;
20
+ notNull: true;
21
+ hasDefault: false;
22
+ isPrimaryKey: true;
23
+ isAutoincrement: false;
24
+ hasRuntimeDefault: false;
25
+ enumValues: [string, ...string[]];
26
+ baseColumn: never;
27
+ identity: undefined;
28
+ generated: undefined;
29
+ }, {}, {}>;
30
+ fetchTier: import("drizzle-orm/pg-core").PgColumn<{
31
+ name: "fetch_tier";
32
+ tableName: "host_records";
33
+ dataType: "number";
34
+ columnType: "PgSmallInt";
35
+ data: number;
36
+ driverParam: string | number;
37
+ notNull: true;
38
+ hasDefault: true;
39
+ isPrimaryKey: false;
40
+ isAutoincrement: false;
41
+ hasRuntimeDefault: false;
42
+ enumValues: undefined;
43
+ baseColumn: never;
44
+ identity: undefined;
45
+ generated: undefined;
46
+ }, {}, {}>;
47
+ fetchTierUpdatedAt: import("drizzle-orm/pg-core").PgColumn<{
48
+ name: "fetch_tier_updated_at";
49
+ tableName: "host_records";
50
+ dataType: "date";
51
+ columnType: "PgTimestamp";
52
+ data: Date;
53
+ driverParam: string;
54
+ notNull: true;
55
+ hasDefault: true;
56
+ isPrimaryKey: false;
57
+ isAutoincrement: false;
58
+ hasRuntimeDefault: false;
59
+ enumValues: undefined;
60
+ baseColumn: never;
61
+ identity: undefined;
62
+ generated: undefined;
63
+ }, {}, {}>;
64
+ preferredProxyProvider: import("drizzle-orm/pg-core").PgColumn<{
65
+ name: "preferred_proxy_provider";
66
+ tableName: "host_records";
67
+ dataType: "string";
68
+ columnType: "PgText";
69
+ data: string;
70
+ driverParam: string;
71
+ notNull: false;
72
+ hasDefault: false;
73
+ isPrimaryKey: false;
74
+ isAutoincrement: false;
75
+ hasRuntimeDefault: false;
76
+ enumValues: [string, ...string[]];
77
+ baseColumn: never;
78
+ identity: undefined;
79
+ generated: undefined;
80
+ }, {}, {}>;
81
+ maxConcurrentFetches: import("drizzle-orm/pg-core").PgColumn<{
82
+ name: "max_concurrent_fetches";
83
+ tableName: "host_records";
84
+ dataType: "number";
85
+ columnType: "PgSmallInt";
86
+ data: number;
87
+ driverParam: string | number;
88
+ notNull: true;
89
+ hasDefault: true;
90
+ isPrimaryKey: false;
91
+ isAutoincrement: false;
92
+ hasRuntimeDefault: false;
93
+ enumValues: undefined;
94
+ baseColumn: never;
95
+ identity: undefined;
96
+ generated: undefined;
97
+ }, {}, {}>;
98
+ observedAntiBot: import("drizzle-orm/pg-core").PgColumn<{
99
+ name: "observed_anti_bot";
100
+ tableName: "host_records";
101
+ dataType: "array";
102
+ columnType: "PgArray";
103
+ data: string[];
104
+ driverParam: string | string[];
105
+ notNull: false;
106
+ hasDefault: false;
107
+ isPrimaryKey: false;
108
+ isAutoincrement: false;
109
+ hasRuntimeDefault: false;
110
+ enumValues: [string, ...string[]];
111
+ baseColumn: import("drizzle-orm").Column<{
112
+ name: "observed_anti_bot";
113
+ tableName: "host_records";
114
+ dataType: "string";
115
+ columnType: "PgText";
116
+ data: string;
117
+ driverParam: string;
118
+ notNull: false;
119
+ hasDefault: false;
120
+ isPrimaryKey: false;
121
+ isAutoincrement: false;
122
+ hasRuntimeDefault: false;
123
+ enumValues: [string, ...string[]];
124
+ baseColumn: never;
125
+ identity: undefined;
126
+ generated: undefined;
127
+ }, object, object>;
128
+ identity: undefined;
129
+ generated: undefined;
130
+ }, {}, {}>;
131
+ robotsStatus: import("drizzle-orm/pg-core").PgColumn<{
132
+ name: "robots_status";
133
+ tableName: "host_records";
134
+ dataType: "string";
135
+ columnType: "PgText";
136
+ data: string;
137
+ driverParam: string;
138
+ notNull: true;
139
+ hasDefault: true;
140
+ isPrimaryKey: false;
141
+ isAutoincrement: false;
142
+ hasRuntimeDefault: false;
143
+ enumValues: [string, ...string[]];
144
+ baseColumn: never;
145
+ identity: undefined;
146
+ generated: undefined;
147
+ }, {}, {}>;
148
+ takedownStatus: import("drizzle-orm/pg-core").PgColumn<{
149
+ name: "takedown_status";
150
+ tableName: "host_records";
151
+ dataType: "string";
152
+ columnType: "PgText";
153
+ data: string;
154
+ driverParam: string;
155
+ notNull: true;
156
+ hasDefault: true;
157
+ isPrimaryKey: false;
158
+ isAutoincrement: false;
159
+ hasRuntimeDefault: false;
160
+ enumValues: [string, ...string[]];
161
+ baseColumn: never;
162
+ identity: undefined;
163
+ generated: undefined;
164
+ }, {}, {}>;
165
+ countryHint: import("drizzle-orm/pg-core").PgColumn<{
166
+ name: "country_hint";
167
+ tableName: "host_records";
168
+ dataType: "string";
169
+ columnType: "PgChar";
170
+ data: string;
171
+ driverParam: string;
172
+ notNull: false;
173
+ hasDefault: false;
174
+ isPrimaryKey: false;
175
+ isAutoincrement: false;
176
+ hasRuntimeDefault: false;
177
+ enumValues: [string, ...string[]];
178
+ baseColumn: never;
179
+ identity: undefined;
180
+ generated: undefined;
181
+ }, {}, {}>;
182
+ notes: import("drizzle-orm/pg-core").PgColumn<{
183
+ name: "notes";
184
+ tableName: "host_records";
185
+ dataType: "string";
186
+ columnType: "PgText";
187
+ data: string;
188
+ driverParam: string;
189
+ notNull: false;
190
+ hasDefault: false;
191
+ isPrimaryKey: false;
192
+ isAutoincrement: false;
193
+ hasRuntimeDefault: false;
194
+ enumValues: [string, ...string[]];
195
+ baseColumn: never;
196
+ identity: undefined;
197
+ generated: undefined;
198
+ }, {}, {}>;
199
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
200
+ name: "created_at";
201
+ tableName: "host_records";
202
+ dataType: "date";
203
+ columnType: "PgTimestamp";
204
+ data: Date;
205
+ driverParam: string;
206
+ notNull: true;
207
+ hasDefault: true;
208
+ isPrimaryKey: false;
209
+ isAutoincrement: false;
210
+ hasRuntimeDefault: false;
211
+ enumValues: undefined;
212
+ baseColumn: never;
213
+ identity: undefined;
214
+ generated: undefined;
215
+ }, {}, {}>;
216
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
217
+ name: "updated_at";
218
+ tableName: "host_records";
219
+ dataType: "date";
220
+ columnType: "PgTimestamp";
221
+ data: Date;
222
+ driverParam: string;
223
+ notNull: true;
224
+ hasDefault: true;
225
+ isPrimaryKey: false;
226
+ isAutoincrement: false;
227
+ hasRuntimeDefault: false;
228
+ enumValues: undefined;
229
+ baseColumn: never;
230
+ identity: undefined;
231
+ generated: undefined;
232
+ }, {}, {}>;
233
+ };
234
+ dialect: "pg";
235
+ }>;
236
+ export type HostRecord = typeof hostRecords.$inferSelect;
237
+ export type NewHostRecord = typeof hostRecords.$inferInsert;
@@ -0,0 +1,42 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { sql } from 'drizzle-orm';
3
+ import { char, index, pgTable, smallint, text, timestamp } from 'drizzle-orm/pg-core';
4
+ /**
5
+ * Per-host config table (Review #4; deferred from Review #2).
6
+ *
7
+ * Drives fetcher tier routing, proxy provider preference, politeness limits,
8
+ * robots-policy tracking, and takedown status. Small (one row per host we've
9
+ * ever fetched), read-heavy — cache in memory in the fetcher workers; refresh
10
+ * every 60 seconds.
11
+ */
12
+ export const hostRecords = pgTable('host_records', {
13
+ // the normalized host, e.g. 'example.com'
14
+ host: text('host').primaryKey(),
15
+ // 0, 1, 2, 3 — the current working tier (Review #4)
16
+ fetchTier: smallint('fetch_tier').notNull().default(1),
17
+ fetchTierUpdatedAt: timestamp('fetch_tier_updated_at', { withTimezone: true })
18
+ .notNull()
19
+ .defaultNow(),
20
+ // e.g. 'decodo_residential_us'; null = default
21
+ preferredProxyProvider: text('preferred_proxy_provider'),
22
+ // politeness cap across all customers (Review #4)
23
+ maxConcurrentFetches: smallint('max_concurrent_fetches').notNull().default(2),
24
+ // ['cloudflare', 'datadome', 'akamai', 'perimeterx']
25
+ observedAntiBot: text('observed_anti_bot').array(),
26
+ // 'allowed' | 'disallowed' | 'unknown'
27
+ robotsStatus: text('robots_status').notNull().default('unknown'),
28
+ // 'active' | 'requested' | 'erased' (Review #2 takedown lifecycle)
29
+ takedownStatus: text('takedown_status').notNull().default('active'),
30
+ // ISO 3166-1 alpha-2 — geo-match proxies
31
+ countryHint: char('country_hint', { length: 2 }),
32
+ // human notes from ops
33
+ notes: text('notes'),
34
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
35
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
36
+ }, (table) => [
37
+ index('idx_host_tier').on(table.fetchTier),
38
+ index('idx_host_takedown')
39
+ .on(table.takedownStatus)
40
+ .where(sql `${table.takedownStatus} != 'active'`),
41
+ ]);
42
+ //# sourceMappingURL=host-records.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-records.js","sourceRoot":"","sources":["../../src/schema/host-records.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEtF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAChC,cAAc,EACd;IACE,0CAA0C;IAC1C,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE;IAC/B,oDAAoD;IACpD,SAAS,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACtD,kBAAkB,EAAE,SAAS,CAAC,uBAAuB,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;SAC3E,OAAO,EAAE;SACT,UAAU,EAAE;IACf,+CAA+C;IAC/C,sBAAsB,EAAE,IAAI,CAAC,0BAA0B,CAAC;IACxD,kDAAkD;IAClD,oBAAoB,EAAE,QAAQ,CAAC,wBAAwB,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7E,qDAAqD;IACrD,eAAe,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC,KAAK,EAAE;IAClD,uCAAuC;IACvC,YAAY,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC;IAChE,mEAAmE;IACnE,cAAc,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC;IACnE,yCAAyC;IACzC,WAAW,EAAE,IAAI,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IAChD,uBAAuB;IACvB,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC;IACpB,SAAS,EAAE,SAAS,CAAC,YAAY,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE;IACjF,SAAS,EAAE,SAAS,CAAC,YAAY,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE;CAClF,EACD,CAAC,KAAK,EAAE,EAAE,CAAC;IACT,KAAK,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC;IAC1C,KAAK,CAAC,mBAAmB,CAAC;SACvB,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC;SACxB,KAAK,CAAC,GAAG,CAAA,GAAG,KAAK,CAAC,cAAc,cAAc,CAAC;CACnD,CACF,CAAC"}
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Customer-facing notification preferences (billing-trust feature).
3
+ *
4
+ * One row per customer; absence of a row means "default opt-in" —
5
+ * notification check queries LEFT JOIN this table and COALESCE the
6
+ * enabled flags to true. Customers can disable specific alerts via
7
+ * an API endpoint (TODO: not yet wired) or by zeroing the threshold.
8
+ *
9
+ * Two alert kinds today:
10
+ * - cost_anomaly: 24h cost > N x 7-day rolling average (N defaults to 2)
11
+ * - delivery_health: webhook failure rate > N% over 6h (N defaults to 25)
12
+ *
13
+ * Both alerts debounce on `last_*_alert_at` — won't re-fire within 12h
14
+ * for the same customer + condition (prevents spam if the condition
15
+ * persists across multiple cron runs).
16
+ */
17
+ export declare const notificationPreferences: import("drizzle-orm/pg-core").PgTableWithColumns<{
18
+ name: "notification_preferences";
19
+ schema: undefined;
20
+ columns: {
21
+ customerId: import("drizzle-orm/pg-core").PgColumn<{
22
+ name: "customer_id";
23
+ tableName: "notification_preferences";
24
+ dataType: "string";
25
+ columnType: "PgUUID";
26
+ data: string;
27
+ driverParam: string;
28
+ notNull: true;
29
+ hasDefault: false;
30
+ isPrimaryKey: true;
31
+ isAutoincrement: false;
32
+ hasRuntimeDefault: false;
33
+ enumValues: undefined;
34
+ baseColumn: never;
35
+ identity: undefined;
36
+ generated: undefined;
37
+ }, {}, {}>;
38
+ costAnomalyEnabled: import("drizzle-orm/pg-core").PgColumn<{
39
+ name: "cost_anomaly_enabled";
40
+ tableName: "notification_preferences";
41
+ dataType: "boolean";
42
+ columnType: "PgBoolean";
43
+ data: boolean;
44
+ driverParam: boolean;
45
+ notNull: true;
46
+ hasDefault: true;
47
+ isPrimaryKey: false;
48
+ isAutoincrement: false;
49
+ hasRuntimeDefault: false;
50
+ enumValues: undefined;
51
+ baseColumn: never;
52
+ identity: undefined;
53
+ generated: undefined;
54
+ }, {}, {}>;
55
+ costAnomalyThresholdMultiplier: import("drizzle-orm/pg-core").PgColumn<{
56
+ name: "cost_anomaly_threshold_multiplier";
57
+ tableName: "notification_preferences";
58
+ dataType: "string";
59
+ columnType: "PgNumeric";
60
+ data: string;
61
+ driverParam: string;
62
+ notNull: true;
63
+ hasDefault: true;
64
+ isPrimaryKey: false;
65
+ isAutoincrement: false;
66
+ hasRuntimeDefault: false;
67
+ enumValues: undefined;
68
+ baseColumn: never;
69
+ identity: undefined;
70
+ generated: undefined;
71
+ }, {}, {}>;
72
+ deliveryHealthEnabled: import("drizzle-orm/pg-core").PgColumn<{
73
+ name: "delivery_health_enabled";
74
+ tableName: "notification_preferences";
75
+ dataType: "boolean";
76
+ columnType: "PgBoolean";
77
+ data: boolean;
78
+ driverParam: boolean;
79
+ notNull: true;
80
+ hasDefault: true;
81
+ isPrimaryKey: false;
82
+ isAutoincrement: false;
83
+ hasRuntimeDefault: false;
84
+ enumValues: undefined;
85
+ baseColumn: never;
86
+ identity: undefined;
87
+ generated: undefined;
88
+ }, {}, {}>;
89
+ deliveryHealthThresholdPct: import("drizzle-orm/pg-core").PgColumn<{
90
+ name: "delivery_health_threshold_pct";
91
+ tableName: "notification_preferences";
92
+ dataType: "number";
93
+ columnType: "PgSmallInt";
94
+ data: number;
95
+ driverParam: string | number;
96
+ notNull: true;
97
+ hasDefault: true;
98
+ isPrimaryKey: false;
99
+ isAutoincrement: false;
100
+ hasRuntimeDefault: false;
101
+ enumValues: undefined;
102
+ baseColumn: never;
103
+ identity: undefined;
104
+ generated: undefined;
105
+ }, {}, {}>;
106
+ emailOverride: import("drizzle-orm/pg-core").PgColumn<{
107
+ name: "email_override";
108
+ tableName: "notification_preferences";
109
+ dataType: "string";
110
+ columnType: "PgText";
111
+ data: string;
112
+ driverParam: string;
113
+ notNull: false;
114
+ hasDefault: false;
115
+ isPrimaryKey: false;
116
+ isAutoincrement: false;
117
+ hasRuntimeDefault: false;
118
+ enumValues: [string, ...string[]];
119
+ baseColumn: never;
120
+ identity: undefined;
121
+ generated: undefined;
122
+ }, {}, {}>;
123
+ lastCostAlertAt: import("drizzle-orm/pg-core").PgColumn<{
124
+ name: "last_cost_alert_at";
125
+ tableName: "notification_preferences";
126
+ dataType: "date";
127
+ columnType: "PgTimestamp";
128
+ data: Date;
129
+ driverParam: string;
130
+ notNull: false;
131
+ hasDefault: false;
132
+ isPrimaryKey: false;
133
+ isAutoincrement: false;
134
+ hasRuntimeDefault: false;
135
+ enumValues: undefined;
136
+ baseColumn: never;
137
+ identity: undefined;
138
+ generated: undefined;
139
+ }, {}, {}>;
140
+ lastDeliveryAlertAt: import("drizzle-orm/pg-core").PgColumn<{
141
+ name: "last_delivery_alert_at";
142
+ tableName: "notification_preferences";
143
+ dataType: "date";
144
+ columnType: "PgTimestamp";
145
+ data: Date;
146
+ driverParam: string;
147
+ notNull: false;
148
+ hasDefault: false;
149
+ isPrimaryKey: false;
150
+ isAutoincrement: false;
151
+ hasRuntimeDefault: false;
152
+ enumValues: undefined;
153
+ baseColumn: never;
154
+ identity: undefined;
155
+ generated: undefined;
156
+ }, {}, {}>;
157
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
158
+ name: "created_at";
159
+ tableName: "notification_preferences";
160
+ dataType: "date";
161
+ columnType: "PgTimestamp";
162
+ data: Date;
163
+ driverParam: string;
164
+ notNull: true;
165
+ hasDefault: true;
166
+ isPrimaryKey: false;
167
+ isAutoincrement: false;
168
+ hasRuntimeDefault: false;
169
+ enumValues: undefined;
170
+ baseColumn: never;
171
+ identity: undefined;
172
+ generated: undefined;
173
+ }, {}, {}>;
174
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
175
+ name: "updated_at";
176
+ tableName: "notification_preferences";
177
+ dataType: "date";
178
+ columnType: "PgTimestamp";
179
+ data: Date;
180
+ driverParam: string;
181
+ notNull: true;
182
+ hasDefault: true;
183
+ isPrimaryKey: false;
184
+ isAutoincrement: false;
185
+ hasRuntimeDefault: false;
186
+ enumValues: undefined;
187
+ baseColumn: never;
188
+ identity: undefined;
189
+ generated: undefined;
190
+ }, {}, {}>;
191
+ };
192
+ dialect: "pg";
193
+ }>;
194
+ export type NotificationPreferences = typeof notificationPreferences.$inferSelect;
195
+ export type NewNotificationPreferences = typeof notificationPreferences.$inferInsert;
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { boolean, numeric, pgTable, smallint, text, timestamp, uuid } from 'drizzle-orm/pg-core';
3
+ import { customers } from './customers.js';
4
+ /**
5
+ * Customer-facing notification preferences (billing-trust feature).
6
+ *
7
+ * One row per customer; absence of a row means "default opt-in" —
8
+ * notification check queries LEFT JOIN this table and COALESCE the
9
+ * enabled flags to true. Customers can disable specific alerts via
10
+ * an API endpoint (TODO: not yet wired) or by zeroing the threshold.
11
+ *
12
+ * Two alert kinds today:
13
+ * - cost_anomaly: 24h cost > N x 7-day rolling average (N defaults to 2)
14
+ * - delivery_health: webhook failure rate > N% over 6h (N defaults to 25)
15
+ *
16
+ * Both alerts debounce on `last_*_alert_at` — won't re-fire within 12h
17
+ * for the same customer + condition (prevents spam if the condition
18
+ * persists across multiple cron runs).
19
+ */
20
+ export const notificationPreferences = pgTable('notification_preferences', {
21
+ customerId: uuid('customer_id')
22
+ .primaryKey()
23
+ .notNull()
24
+ .references(() => customers.id),
25
+ // ── Cost anomaly settings ──
26
+ costAnomalyEnabled: boolean('cost_anomaly_enabled').notNull().default(true),
27
+ // Multiplier — alert when 24h cost > this × 7-day daily average.
28
+ // numeric(4,2) → max 99.99; 0 disables this alert.
29
+ costAnomalyThresholdMultiplier: numeric('cost_anomaly_threshold_multiplier', {
30
+ precision: 4,
31
+ scale: 2,
32
+ })
33
+ .notNull()
34
+ .default('2.00'),
35
+ // ── Delivery health settings ──
36
+ deliveryHealthEnabled: boolean('delivery_health_enabled').notNull().default(true),
37
+ // Percent (0-100). Alert when failure rate over 6h exceeds this.
38
+ deliveryHealthThresholdPct: smallint('delivery_health_threshold_pct').notNull().default(25),
39
+ // ── Routing ──
40
+ // When set, notifications go here instead of customers.email. Useful
41
+ // for routing to a billing@ alias separate from the API contact.
42
+ emailOverride: text('email_override'),
43
+ // ── Debounce state ──
44
+ lastCostAlertAt: timestamp('last_cost_alert_at', { withTimezone: true }),
45
+ lastDeliveryAlertAt: timestamp('last_delivery_alert_at', { withTimezone: true }),
46
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
47
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
48
+ });
49
+ //# sourceMappingURL=notification-preferences.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-preferences.js","sourceRoot":"","sources":["../../src/schema/notification-preferences.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AACjG,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,OAAO,CAAC,0BAA0B,EAAE;IACzE,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC;SAC5B,UAAU,EAAE;SACZ,OAAO,EAAE;SACT,UAAU,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC;IACjC,8BAA8B;IAC9B,kBAAkB,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3E,iEAAiE;IACjE,mDAAmD;IACnD,8BAA8B,EAAE,OAAO,CAAC,mCAAmC,EAAE;QAC3E,SAAS,EAAE,CAAC;QACZ,KAAK,EAAE,CAAC;KACT,CAAC;SACC,OAAO,EAAE;SACT,OAAO,CAAC,MAAM,CAAC;IAClB,iCAAiC;IACjC,qBAAqB,EAAE,OAAO,CAAC,yBAAyB,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IACjF,iEAAiE;IACjE,0BAA0B,EAAE,QAAQ,CAAC,+BAA+B,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IAC3F,gBAAgB;IAChB,qEAAqE;IACrE,iEAAiE;IACjE,aAAa,EAAE,IAAI,CAAC,gBAAgB,CAAC;IACrC,uBAAuB;IACvB,eAAe,EAAE,SAAS,CAAC,oBAAoB,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IACxE,mBAAmB,EAAE,SAAS,CAAC,wBAAwB,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IAChF,SAAS,EAAE,SAAS,CAAC,YAAY,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE;IACjF,SAAS,EAAE,SAAS,CAAC,YAAY,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE;CAClF,CAAC,CAAC"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * scrape_cache — per-customer URL-level cache for /scrape maxAge (Phase A.5).
3
+ *
4
+ * Composite PK on (customer_id, url_canonical). One row per URL per
5
+ * customer — upserted on every fresh scrape so we always reflect the
6
+ * latest fetch. Cross-customer sharing happens deeper at the extractor
7
+ * cache layer (content_hash + schema_hash).
8
+ */
9
+ export declare const scrapeCache: import("drizzle-orm/pg-core").PgTableWithColumns<{
10
+ name: "scrape_cache";
11
+ schema: undefined;
12
+ columns: {
13
+ customerId: import("drizzle-orm/pg-core").PgColumn<{
14
+ name: "customer_id";
15
+ tableName: "scrape_cache";
16
+ dataType: "string";
17
+ columnType: "PgUUID";
18
+ data: string;
19
+ driverParam: string;
20
+ notNull: true;
21
+ hasDefault: false;
22
+ isPrimaryKey: false;
23
+ isAutoincrement: false;
24
+ hasRuntimeDefault: false;
25
+ enumValues: undefined;
26
+ baseColumn: never;
27
+ identity: undefined;
28
+ generated: undefined;
29
+ }, {}, {}>;
30
+ urlCanonical: import("drizzle-orm/pg-core").PgColumn<{
31
+ name: "url_canonical";
32
+ tableName: "scrape_cache";
33
+ dataType: "string";
34
+ columnType: "PgText";
35
+ data: string;
36
+ driverParam: string;
37
+ notNull: true;
38
+ hasDefault: false;
39
+ isPrimaryKey: false;
40
+ isAutoincrement: false;
41
+ hasRuntimeDefault: false;
42
+ enumValues: [string, ...string[]];
43
+ baseColumn: never;
44
+ identity: undefined;
45
+ generated: undefined;
46
+ }, {}, {}>;
47
+ extractionId: import("drizzle-orm/pg-core").PgColumn<{
48
+ name: "extraction_id";
49
+ tableName: "scrape_cache";
50
+ dataType: "string";
51
+ columnType: "PgUUID";
52
+ data: string;
53
+ driverParam: string;
54
+ notNull: false;
55
+ hasDefault: false;
56
+ isPrimaryKey: false;
57
+ isAutoincrement: false;
58
+ hasRuntimeDefault: false;
59
+ enumValues: undefined;
60
+ baseColumn: never;
61
+ identity: undefined;
62
+ generated: undefined;
63
+ }, {}, {}>;
64
+ contentHash: import("drizzle-orm/pg-core").PgColumn<{
65
+ name: "content_hash";
66
+ tableName: "scrape_cache";
67
+ dataType: "string";
68
+ columnType: "PgText";
69
+ data: string;
70
+ driverParam: string;
71
+ notNull: true;
72
+ hasDefault: false;
73
+ isPrimaryKey: false;
74
+ isAutoincrement: false;
75
+ hasRuntimeDefault: false;
76
+ enumValues: [string, ...string[]];
77
+ baseColumn: never;
78
+ identity: undefined;
79
+ generated: undefined;
80
+ }, {}, {}>;
81
+ finalUrl: import("drizzle-orm/pg-core").PgColumn<{
82
+ name: "final_url";
83
+ tableName: "scrape_cache";
84
+ dataType: "string";
85
+ columnType: "PgText";
86
+ data: string;
87
+ driverParam: string;
88
+ notNull: true;
89
+ hasDefault: false;
90
+ isPrimaryKey: false;
91
+ isAutoincrement: false;
92
+ hasRuntimeDefault: false;
93
+ enumValues: [string, ...string[]];
94
+ baseColumn: never;
95
+ identity: undefined;
96
+ generated: undefined;
97
+ }, {}, {}>;
98
+ responseStatus: import("drizzle-orm/pg-core").PgColumn<{
99
+ name: "response_status";
100
+ tableName: "scrape_cache";
101
+ dataType: "number";
102
+ columnType: "PgInteger";
103
+ data: number;
104
+ driverParam: string | number;
105
+ notNull: true;
106
+ hasDefault: false;
107
+ isPrimaryKey: false;
108
+ isAutoincrement: false;
109
+ hasRuntimeDefault: false;
110
+ enumValues: undefined;
111
+ baseColumn: never;
112
+ identity: undefined;
113
+ generated: undefined;
114
+ }, {}, {}>;
115
+ fetchedAt: import("drizzle-orm/pg-core").PgColumn<{
116
+ name: "fetched_at";
117
+ tableName: "scrape_cache";
118
+ dataType: "date";
119
+ columnType: "PgTimestamp";
120
+ data: Date;
121
+ driverParam: string;
122
+ notNull: true;
123
+ hasDefault: true;
124
+ isPrimaryKey: false;
125
+ isAutoincrement: false;
126
+ hasRuntimeDefault: false;
127
+ enumValues: undefined;
128
+ baseColumn: never;
129
+ identity: undefined;
130
+ generated: undefined;
131
+ }, {}, {}>;
132
+ };
133
+ dialect: "pg";
134
+ }>;
135
+ export type ScrapeCacheRow = typeof scrapeCache.$inferSelect;
136
+ export type NewScrapeCacheRow = typeof scrapeCache.$inferInsert;