@hasna/microservices 0.0.3 → 0.0.4
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/bin/index.js +63 -0
- package/bin/mcp.js +63 -0
- package/dist/index.js +63 -0
- package/microservices/microservice-ads/package.json +27 -0
- package/microservices/microservice-ads/src/cli/index.ts +407 -0
- package/microservices/microservice-ads/src/db/campaigns.ts +493 -0
- package/microservices/microservice-ads/src/db/database.ts +93 -0
- package/microservices/microservice-ads/src/db/migrations.ts +60 -0
- package/microservices/microservice-ads/src/index.ts +39 -0
- package/microservices/microservice-ads/src/mcp/index.ts +320 -0
- package/microservices/microservice-contracts/package.json +27 -0
- package/microservices/microservice-contracts/src/cli/index.ts +383 -0
- package/microservices/microservice-contracts/src/db/contracts.ts +496 -0
- package/microservices/microservice-contracts/src/db/database.ts +93 -0
- package/microservices/microservice-contracts/src/db/migrations.ts +58 -0
- package/microservices/microservice-contracts/src/index.ts +43 -0
- package/microservices/microservice-contracts/src/mcp/index.ts +308 -0
- package/microservices/microservice-domains/package.json +27 -0
- package/microservices/microservice-domains/src/cli/index.ts +438 -0
- package/microservices/microservice-domains/src/db/database.ts +93 -0
- package/microservices/microservice-domains/src/db/domains.ts +551 -0
- package/microservices/microservice-domains/src/db/migrations.ts +60 -0
- package/microservices/microservice-domains/src/index.ts +44 -0
- package/microservices/microservice-domains/src/mcp/index.ts +368 -0
- package/microservices/microservice-hiring/package.json +27 -0
- package/microservices/microservice-hiring/src/cli/index.ts +431 -0
- package/microservices/microservice-hiring/src/db/database.ts +93 -0
- package/microservices/microservice-hiring/src/db/hiring.ts +582 -0
- package/microservices/microservice-hiring/src/db/migrations.ts +68 -0
- package/microservices/microservice-hiring/src/index.ts +51 -0
- package/microservices/microservice-hiring/src/mcp/index.ts +464 -0
- package/microservices/microservice-payments/package.json +27 -0
- package/microservices/microservice-payments/src/cli/index.ts +357 -0
- package/microservices/microservice-payments/src/db/database.ts +93 -0
- package/microservices/microservice-payments/src/db/migrations.ts +63 -0
- package/microservices/microservice-payments/src/db/payments.ts +652 -0
- package/microservices/microservice-payments/src/index.ts +51 -0
- package/microservices/microservice-payments/src/mcp/index.ts +460 -0
- package/microservices/microservice-payroll/package.json +27 -0
- package/microservices/microservice-payroll/src/cli/index.ts +374 -0
- package/microservices/microservice-payroll/src/db/database.ts +93 -0
- package/microservices/microservice-payroll/src/db/migrations.ts +69 -0
- package/microservices/microservice-payroll/src/db/payroll.ts +741 -0
- package/microservices/microservice-payroll/src/index.ts +48 -0
- package/microservices/microservice-payroll/src/mcp/index.ts +420 -0
- package/microservices/microservice-shipping/package.json +27 -0
- package/microservices/microservice-shipping/src/cli/index.ts +398 -0
- package/microservices/microservice-shipping/src/db/database.ts +93 -0
- package/microservices/microservice-shipping/src/db/migrations.ts +61 -0
- package/microservices/microservice-shipping/src/db/shipping.ts +643 -0
- package/microservices/microservice-shipping/src/index.ts +53 -0
- package/microservices/microservice-shipping/src/mcp/index.ts +385 -0
- package/microservices/microservice-social/package.json +27 -0
- package/microservices/microservice-social/src/cli/index.ts +447 -0
- package/microservices/microservice-social/src/db/database.ts +93 -0
- package/microservices/microservice-social/src/db/migrations.ts +55 -0
- package/microservices/microservice-social/src/db/social.ts +672 -0
- package/microservices/microservice-social/src/index.ts +46 -0
- package/microservices/microservice-social/src/mcp/index.ts +435 -0
- package/microservices/microservice-subscriptions/package.json +27 -0
- package/microservices/microservice-subscriptions/src/cli/index.ts +400 -0
- package/microservices/microservice-subscriptions/src/db/database.ts +93 -0
- package/microservices/microservice-subscriptions/src/db/migrations.ts +57 -0
- package/microservices/microservice-subscriptions/src/db/subscriptions.ts +692 -0
- package/microservices/microservice-subscriptions/src/index.ts +41 -0
- package/microservices/microservice-subscriptions/src/mcp/index.ts +365 -0
- package/package.json +1 -1
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Campaign, Ad Group, and Ad CRUD operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getDatabase } from "./database.js";
|
|
6
|
+
|
|
7
|
+
// --- Types ---
|
|
8
|
+
|
|
9
|
+
export type Platform = "google" | "meta" | "linkedin" | "tiktok";
|
|
10
|
+
export type CampaignStatus = "draft" | "active" | "paused" | "completed";
|
|
11
|
+
|
|
12
|
+
export interface Campaign {
|
|
13
|
+
id: string;
|
|
14
|
+
platform: Platform;
|
|
15
|
+
name: string;
|
|
16
|
+
status: CampaignStatus;
|
|
17
|
+
budget_daily: number;
|
|
18
|
+
budget_total: number;
|
|
19
|
+
spend: number;
|
|
20
|
+
impressions: number;
|
|
21
|
+
clicks: number;
|
|
22
|
+
conversions: number;
|
|
23
|
+
roas: number;
|
|
24
|
+
start_date: string | null;
|
|
25
|
+
end_date: string | null;
|
|
26
|
+
created_at: string;
|
|
27
|
+
updated_at: string;
|
|
28
|
+
metadata: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface CampaignRow {
|
|
32
|
+
id: string;
|
|
33
|
+
platform: Platform;
|
|
34
|
+
name: string;
|
|
35
|
+
status: CampaignStatus;
|
|
36
|
+
budget_daily: number;
|
|
37
|
+
budget_total: number;
|
|
38
|
+
spend: number;
|
|
39
|
+
impressions: number;
|
|
40
|
+
clicks: number;
|
|
41
|
+
conversions: number;
|
|
42
|
+
roas: number;
|
|
43
|
+
start_date: string | null;
|
|
44
|
+
end_date: string | null;
|
|
45
|
+
created_at: string;
|
|
46
|
+
updated_at: string;
|
|
47
|
+
metadata: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function rowToCampaign(row: CampaignRow): Campaign {
|
|
51
|
+
return {
|
|
52
|
+
...row,
|
|
53
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AdGroup {
|
|
58
|
+
id: string;
|
|
59
|
+
campaign_id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
targeting: Record<string, unknown>;
|
|
62
|
+
status: CampaignStatus;
|
|
63
|
+
created_at: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface AdGroupRow {
|
|
67
|
+
id: string;
|
|
68
|
+
campaign_id: string;
|
|
69
|
+
name: string;
|
|
70
|
+
targeting: string;
|
|
71
|
+
status: CampaignStatus;
|
|
72
|
+
created_at: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function rowToAdGroup(row: AdGroupRow): AdGroup {
|
|
76
|
+
return {
|
|
77
|
+
...row,
|
|
78
|
+
targeting: JSON.parse(row.targeting || "{}"),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface Ad {
|
|
83
|
+
id: string;
|
|
84
|
+
ad_group_id: string;
|
|
85
|
+
headline: string;
|
|
86
|
+
description: string | null;
|
|
87
|
+
creative_url: string | null;
|
|
88
|
+
status: CampaignStatus;
|
|
89
|
+
metrics: Record<string, unknown>;
|
|
90
|
+
created_at: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface AdRow {
|
|
94
|
+
id: string;
|
|
95
|
+
ad_group_id: string;
|
|
96
|
+
headline: string;
|
|
97
|
+
description: string | null;
|
|
98
|
+
creative_url: string | null;
|
|
99
|
+
status: CampaignStatus;
|
|
100
|
+
metrics: string;
|
|
101
|
+
created_at: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function rowToAd(row: AdRow): Ad {
|
|
105
|
+
return {
|
|
106
|
+
...row,
|
|
107
|
+
metrics: JSON.parse(row.metrics || "{}"),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- Campaign CRUD ---
|
|
112
|
+
|
|
113
|
+
export interface CreateCampaignInput {
|
|
114
|
+
platform: Platform;
|
|
115
|
+
name: string;
|
|
116
|
+
status?: CampaignStatus;
|
|
117
|
+
budget_daily?: number;
|
|
118
|
+
budget_total?: number;
|
|
119
|
+
spend?: number;
|
|
120
|
+
impressions?: number;
|
|
121
|
+
clicks?: number;
|
|
122
|
+
conversions?: number;
|
|
123
|
+
roas?: number;
|
|
124
|
+
start_date?: string;
|
|
125
|
+
end_date?: string;
|
|
126
|
+
metadata?: Record<string, unknown>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function createCampaign(input: CreateCampaignInput): Campaign {
|
|
130
|
+
const db = getDatabase();
|
|
131
|
+
const id = crypto.randomUUID();
|
|
132
|
+
const metadata = JSON.stringify(input.metadata || {});
|
|
133
|
+
|
|
134
|
+
db.prepare(
|
|
135
|
+
`INSERT INTO campaigns (id, platform, name, status, budget_daily, budget_total, spend, impressions, clicks, conversions, roas, start_date, end_date, metadata)
|
|
136
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
137
|
+
).run(
|
|
138
|
+
id,
|
|
139
|
+
input.platform,
|
|
140
|
+
input.name,
|
|
141
|
+
input.status || "draft",
|
|
142
|
+
input.budget_daily ?? 0,
|
|
143
|
+
input.budget_total ?? 0,
|
|
144
|
+
input.spend ?? 0,
|
|
145
|
+
input.impressions ?? 0,
|
|
146
|
+
input.clicks ?? 0,
|
|
147
|
+
input.conversions ?? 0,
|
|
148
|
+
input.roas ?? 0,
|
|
149
|
+
input.start_date || null,
|
|
150
|
+
input.end_date || null,
|
|
151
|
+
metadata
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return getCampaign(id)!;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function getCampaign(id: string): Campaign | null {
|
|
158
|
+
const db = getDatabase();
|
|
159
|
+
const row = db.prepare("SELECT * FROM campaigns WHERE id = ?").get(id) as CampaignRow | null;
|
|
160
|
+
return row ? rowToCampaign(row) : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface ListCampaignsOptions {
|
|
164
|
+
platform?: Platform;
|
|
165
|
+
status?: CampaignStatus;
|
|
166
|
+
search?: string;
|
|
167
|
+
limit?: number;
|
|
168
|
+
offset?: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function listCampaigns(options: ListCampaignsOptions = {}): Campaign[] {
|
|
172
|
+
const db = getDatabase();
|
|
173
|
+
const conditions: string[] = [];
|
|
174
|
+
const params: unknown[] = [];
|
|
175
|
+
|
|
176
|
+
if (options.platform) {
|
|
177
|
+
conditions.push("platform = ?");
|
|
178
|
+
params.push(options.platform);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (options.status) {
|
|
182
|
+
conditions.push("status = ?");
|
|
183
|
+
params.push(options.status);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (options.search) {
|
|
187
|
+
conditions.push("(name LIKE ?)");
|
|
188
|
+
const q = `%${options.search}%`;
|
|
189
|
+
params.push(q);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let sql = "SELECT * FROM campaigns";
|
|
193
|
+
if (conditions.length > 0) {
|
|
194
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
195
|
+
}
|
|
196
|
+
sql += " ORDER BY created_at DESC";
|
|
197
|
+
|
|
198
|
+
if (options.limit) {
|
|
199
|
+
sql += " LIMIT ?";
|
|
200
|
+
params.push(options.limit);
|
|
201
|
+
}
|
|
202
|
+
if (options.offset) {
|
|
203
|
+
sql += " OFFSET ?";
|
|
204
|
+
params.push(options.offset);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const rows = db.prepare(sql).all(...params) as CampaignRow[];
|
|
208
|
+
return rows.map(rowToCampaign);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface UpdateCampaignInput {
|
|
212
|
+
platform?: Platform;
|
|
213
|
+
name?: string;
|
|
214
|
+
status?: CampaignStatus;
|
|
215
|
+
budget_daily?: number;
|
|
216
|
+
budget_total?: number;
|
|
217
|
+
spend?: number;
|
|
218
|
+
impressions?: number;
|
|
219
|
+
clicks?: number;
|
|
220
|
+
conversions?: number;
|
|
221
|
+
roas?: number;
|
|
222
|
+
start_date?: string | null;
|
|
223
|
+
end_date?: string | null;
|
|
224
|
+
metadata?: Record<string, unknown>;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function updateCampaign(
|
|
228
|
+
id: string,
|
|
229
|
+
input: UpdateCampaignInput
|
|
230
|
+
): Campaign | null {
|
|
231
|
+
const db = getDatabase();
|
|
232
|
+
const existing = getCampaign(id);
|
|
233
|
+
if (!existing) return null;
|
|
234
|
+
|
|
235
|
+
const sets: string[] = [];
|
|
236
|
+
const params: unknown[] = [];
|
|
237
|
+
|
|
238
|
+
if (input.platform !== undefined) {
|
|
239
|
+
sets.push("platform = ?");
|
|
240
|
+
params.push(input.platform);
|
|
241
|
+
}
|
|
242
|
+
if (input.name !== undefined) {
|
|
243
|
+
sets.push("name = ?");
|
|
244
|
+
params.push(input.name);
|
|
245
|
+
}
|
|
246
|
+
if (input.status !== undefined) {
|
|
247
|
+
sets.push("status = ?");
|
|
248
|
+
params.push(input.status);
|
|
249
|
+
}
|
|
250
|
+
if (input.budget_daily !== undefined) {
|
|
251
|
+
sets.push("budget_daily = ?");
|
|
252
|
+
params.push(input.budget_daily);
|
|
253
|
+
}
|
|
254
|
+
if (input.budget_total !== undefined) {
|
|
255
|
+
sets.push("budget_total = ?");
|
|
256
|
+
params.push(input.budget_total);
|
|
257
|
+
}
|
|
258
|
+
if (input.spend !== undefined) {
|
|
259
|
+
sets.push("spend = ?");
|
|
260
|
+
params.push(input.spend);
|
|
261
|
+
}
|
|
262
|
+
if (input.impressions !== undefined) {
|
|
263
|
+
sets.push("impressions = ?");
|
|
264
|
+
params.push(input.impressions);
|
|
265
|
+
}
|
|
266
|
+
if (input.clicks !== undefined) {
|
|
267
|
+
sets.push("clicks = ?");
|
|
268
|
+
params.push(input.clicks);
|
|
269
|
+
}
|
|
270
|
+
if (input.conversions !== undefined) {
|
|
271
|
+
sets.push("conversions = ?");
|
|
272
|
+
params.push(input.conversions);
|
|
273
|
+
}
|
|
274
|
+
if (input.roas !== undefined) {
|
|
275
|
+
sets.push("roas = ?");
|
|
276
|
+
params.push(input.roas);
|
|
277
|
+
}
|
|
278
|
+
if (input.start_date !== undefined) {
|
|
279
|
+
sets.push("start_date = ?");
|
|
280
|
+
params.push(input.start_date);
|
|
281
|
+
}
|
|
282
|
+
if (input.end_date !== undefined) {
|
|
283
|
+
sets.push("end_date = ?");
|
|
284
|
+
params.push(input.end_date);
|
|
285
|
+
}
|
|
286
|
+
if (input.metadata !== undefined) {
|
|
287
|
+
sets.push("metadata = ?");
|
|
288
|
+
params.push(JSON.stringify(input.metadata));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (sets.length === 0) return existing;
|
|
292
|
+
|
|
293
|
+
sets.push("updated_at = datetime('now')");
|
|
294
|
+
params.push(id);
|
|
295
|
+
|
|
296
|
+
db.prepare(
|
|
297
|
+
`UPDATE campaigns SET ${sets.join(", ")} WHERE id = ?`
|
|
298
|
+
).run(...params);
|
|
299
|
+
|
|
300
|
+
return getCampaign(id);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function deleteCampaign(id: string): boolean {
|
|
304
|
+
const db = getDatabase();
|
|
305
|
+
const result = db.prepare("DELETE FROM campaigns WHERE id = ?").run(id);
|
|
306
|
+
return result.changes > 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function pauseCampaign(id: string): Campaign | null {
|
|
310
|
+
return updateCampaign(id, { status: "paused" });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function resumeCampaign(id: string): Campaign | null {
|
|
314
|
+
return updateCampaign(id, { status: "active" });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function countCampaigns(): number {
|
|
318
|
+
const db = getDatabase();
|
|
319
|
+
const row = db.prepare("SELECT COUNT(*) as count FROM campaigns").get() as { count: number };
|
|
320
|
+
return row.count;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// --- Ad Group CRUD ---
|
|
324
|
+
|
|
325
|
+
export interface CreateAdGroupInput {
|
|
326
|
+
campaign_id: string;
|
|
327
|
+
name: string;
|
|
328
|
+
targeting?: Record<string, unknown>;
|
|
329
|
+
status?: CampaignStatus;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function createAdGroup(input: CreateAdGroupInput): AdGroup {
|
|
333
|
+
const db = getDatabase();
|
|
334
|
+
const id = crypto.randomUUID();
|
|
335
|
+
const targeting = JSON.stringify(input.targeting || {});
|
|
336
|
+
|
|
337
|
+
db.prepare(
|
|
338
|
+
`INSERT INTO ad_groups (id, campaign_id, name, targeting, status)
|
|
339
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
340
|
+
).run(
|
|
341
|
+
id,
|
|
342
|
+
input.campaign_id,
|
|
343
|
+
input.name,
|
|
344
|
+
targeting,
|
|
345
|
+
input.status || "draft"
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
return getAdGroup(id)!;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function getAdGroup(id: string): AdGroup | null {
|
|
352
|
+
const db = getDatabase();
|
|
353
|
+
const row = db.prepare("SELECT * FROM ad_groups WHERE id = ?").get(id) as AdGroupRow | null;
|
|
354
|
+
return row ? rowToAdGroup(row) : null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function listAdGroups(campaign_id?: string): AdGroup[] {
|
|
358
|
+
const db = getDatabase();
|
|
359
|
+
let sql = "SELECT * FROM ad_groups";
|
|
360
|
+
const params: unknown[] = [];
|
|
361
|
+
|
|
362
|
+
if (campaign_id) {
|
|
363
|
+
sql += " WHERE campaign_id = ?";
|
|
364
|
+
params.push(campaign_id);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
sql += " ORDER BY created_at DESC";
|
|
368
|
+
|
|
369
|
+
const rows = db.prepare(sql).all(...params) as AdGroupRow[];
|
|
370
|
+
return rows.map(rowToAdGroup);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function deleteAdGroup(id: string): boolean {
|
|
374
|
+
const db = getDatabase();
|
|
375
|
+
const result = db.prepare("DELETE FROM ad_groups WHERE id = ?").run(id);
|
|
376
|
+
return result.changes > 0;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// --- Ad CRUD ---
|
|
380
|
+
|
|
381
|
+
export interface CreateAdInput {
|
|
382
|
+
ad_group_id: string;
|
|
383
|
+
headline: string;
|
|
384
|
+
description?: string;
|
|
385
|
+
creative_url?: string;
|
|
386
|
+
status?: CampaignStatus;
|
|
387
|
+
metrics?: Record<string, unknown>;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function createAd(input: CreateAdInput): Ad {
|
|
391
|
+
const db = getDatabase();
|
|
392
|
+
const id = crypto.randomUUID();
|
|
393
|
+
const metrics = JSON.stringify(input.metrics || {});
|
|
394
|
+
|
|
395
|
+
db.prepare(
|
|
396
|
+
`INSERT INTO ads (id, ad_group_id, headline, description, creative_url, status, metrics)
|
|
397
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
398
|
+
).run(
|
|
399
|
+
id,
|
|
400
|
+
input.ad_group_id,
|
|
401
|
+
input.headline,
|
|
402
|
+
input.description || null,
|
|
403
|
+
input.creative_url || null,
|
|
404
|
+
input.status || "draft",
|
|
405
|
+
metrics
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
return getAd(id)!;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function getAd(id: string): Ad | null {
|
|
412
|
+
const db = getDatabase();
|
|
413
|
+
const row = db.prepare("SELECT * FROM ads WHERE id = ?").get(id) as AdRow | null;
|
|
414
|
+
return row ? rowToAd(row) : null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function listAds(ad_group_id?: string): Ad[] {
|
|
418
|
+
const db = getDatabase();
|
|
419
|
+
let sql = "SELECT * FROM ads";
|
|
420
|
+
const params: unknown[] = [];
|
|
421
|
+
|
|
422
|
+
if (ad_group_id) {
|
|
423
|
+
sql += " WHERE ad_group_id = ?";
|
|
424
|
+
params.push(ad_group_id);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
sql += " ORDER BY created_at DESC";
|
|
428
|
+
|
|
429
|
+
const rows = db.prepare(sql).all(...params) as AdRow[];
|
|
430
|
+
return rows.map(rowToAd);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function deleteAd(id: string): boolean {
|
|
434
|
+
const db = getDatabase();
|
|
435
|
+
const result = db.prepare("DELETE FROM ads WHERE id = ?").run(id);
|
|
436
|
+
return result.changes > 0;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// --- Aggregation helpers ---
|
|
440
|
+
|
|
441
|
+
export interface CampaignStats {
|
|
442
|
+
total_campaigns: number;
|
|
443
|
+
active_campaigns: number;
|
|
444
|
+
total_spend: number;
|
|
445
|
+
total_impressions: number;
|
|
446
|
+
total_clicks: number;
|
|
447
|
+
total_conversions: number;
|
|
448
|
+
avg_roas: number;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function getCampaignStats(): CampaignStats {
|
|
452
|
+
const db = getDatabase();
|
|
453
|
+
const row = db.prepare(`
|
|
454
|
+
SELECT
|
|
455
|
+
COUNT(*) as total_campaigns,
|
|
456
|
+
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_campaigns,
|
|
457
|
+
COALESCE(SUM(spend), 0) as total_spend,
|
|
458
|
+
COALESCE(SUM(impressions), 0) as total_impressions,
|
|
459
|
+
COALESCE(SUM(clicks), 0) as total_clicks,
|
|
460
|
+
COALESCE(SUM(conversions), 0) as total_conversions,
|
|
461
|
+
COALESCE(AVG(CASE WHEN roas > 0 THEN roas END), 0) as avg_roas
|
|
462
|
+
FROM campaigns
|
|
463
|
+
`).get() as CampaignStats;
|
|
464
|
+
return row;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export interface SpendByPlatform {
|
|
468
|
+
platform: Platform;
|
|
469
|
+
total_spend: number;
|
|
470
|
+
campaign_count: number;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function getSpendByPlatform(): SpendByPlatform[] {
|
|
474
|
+
const db = getDatabase();
|
|
475
|
+
const rows = db.prepare(`
|
|
476
|
+
SELECT
|
|
477
|
+
platform,
|
|
478
|
+
COALESCE(SUM(spend), 0) as total_spend,
|
|
479
|
+
COUNT(*) as campaign_count
|
|
480
|
+
FROM campaigns
|
|
481
|
+
GROUP BY platform
|
|
482
|
+
ORDER BY total_spend DESC
|
|
483
|
+
`).all() as SpendByPlatform[];
|
|
484
|
+
return rows;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function getPlatforms(): string[] {
|
|
488
|
+
const db = getDatabase();
|
|
489
|
+
const rows = db.prepare(
|
|
490
|
+
"SELECT DISTINCT platform FROM campaigns ORDER BY platform"
|
|
491
|
+
).all() as { platform: string }[];
|
|
492
|
+
return rows.map((r) => r.platform);
|
|
493
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database connection for microservice-ads
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Database } from "bun:sqlite";
|
|
6
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { MIGRATIONS } from "./migrations.js";
|
|
9
|
+
|
|
10
|
+
let _db: Database | null = null;
|
|
11
|
+
|
|
12
|
+
function getDbPath(): string {
|
|
13
|
+
// Environment variable override
|
|
14
|
+
if (process.env["MICROSERVICES_DIR"]) {
|
|
15
|
+
return join(process.env["MICROSERVICES_DIR"], "microservice-ads", "data.db");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check for .microservices in current or parent directories
|
|
19
|
+
let dir = resolve(process.cwd());
|
|
20
|
+
while (true) {
|
|
21
|
+
const candidate = join(dir, ".microservices", "microservice-ads", "data.db");
|
|
22
|
+
const msDir = join(dir, ".microservices");
|
|
23
|
+
if (existsSync(msDir)) return candidate;
|
|
24
|
+
const parent = dirname(dir);
|
|
25
|
+
if (parent === dir) break;
|
|
26
|
+
dir = parent;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Global fallback
|
|
30
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
31
|
+
return join(home, ".microservices", "microservice-ads", "data.db");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ensureDir(filePath: string): void {
|
|
35
|
+
const dir = dirname(resolve(filePath));
|
|
36
|
+
if (!existsSync(dir)) {
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getDatabase(): Database {
|
|
42
|
+
if (_db) return _db;
|
|
43
|
+
|
|
44
|
+
const dbPath = getDbPath();
|
|
45
|
+
ensureDir(dbPath);
|
|
46
|
+
|
|
47
|
+
_db = new Database(dbPath);
|
|
48
|
+
_db.exec("PRAGMA journal_mode = WAL");
|
|
49
|
+
_db.exec("PRAGMA foreign_keys = ON");
|
|
50
|
+
|
|
51
|
+
// Create migrations table
|
|
52
|
+
_db.exec(`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
54
|
+
id INTEGER PRIMARY KEY,
|
|
55
|
+
name TEXT NOT NULL,
|
|
56
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
57
|
+
)
|
|
58
|
+
`);
|
|
59
|
+
|
|
60
|
+
// Apply pending migrations
|
|
61
|
+
const applied = _db
|
|
62
|
+
.query("SELECT id FROM _migrations ORDER BY id")
|
|
63
|
+
.all() as { id: number }[];
|
|
64
|
+
const appliedIds = new Set(applied.map((r) => r.id));
|
|
65
|
+
|
|
66
|
+
for (const migration of MIGRATIONS) {
|
|
67
|
+
if (appliedIds.has(migration.id)) continue;
|
|
68
|
+
|
|
69
|
+
_db.exec("BEGIN");
|
|
70
|
+
try {
|
|
71
|
+
_db.exec(migration.sql);
|
|
72
|
+
_db.prepare("INSERT INTO _migrations (id, name) VALUES (?, ?)").run(
|
|
73
|
+
migration.id,
|
|
74
|
+
migration.name
|
|
75
|
+
);
|
|
76
|
+
_db.exec("COMMIT");
|
|
77
|
+
} catch (error) {
|
|
78
|
+
_db.exec("ROLLBACK");
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Migration ${migration.id} (${migration.name}) failed: ${error instanceof Error ? error.message : String(error)}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return _db;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function closeDatabase(): void {
|
|
89
|
+
if (_db) {
|
|
90
|
+
_db.close();
|
|
91
|
+
_db = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface MigrationEntry {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
sql: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const MIGRATIONS: MigrationEntry[] = [
|
|
8
|
+
{
|
|
9
|
+
id: 1,
|
|
10
|
+
name: "initial_schema",
|
|
11
|
+
sql: `
|
|
12
|
+
CREATE TABLE IF NOT EXISTS campaigns (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
platform TEXT NOT NULL CHECK (platform IN ('google', 'meta', 'linkedin', 'tiktok')),
|
|
15
|
+
name TEXT NOT NULL,
|
|
16
|
+
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'active', 'paused', 'completed')),
|
|
17
|
+
budget_daily REAL NOT NULL DEFAULT 0,
|
|
18
|
+
budget_total REAL NOT NULL DEFAULT 0,
|
|
19
|
+
spend REAL NOT NULL DEFAULT 0,
|
|
20
|
+
impressions INTEGER NOT NULL DEFAULT 0,
|
|
21
|
+
clicks INTEGER NOT NULL DEFAULT 0,
|
|
22
|
+
conversions INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
roas REAL NOT NULL DEFAULT 0,
|
|
24
|
+
start_date TEXT,
|
|
25
|
+
end_date TEXT,
|
|
26
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
27
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
28
|
+
metadata TEXT NOT NULL DEFAULT '{}'
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS ad_groups (
|
|
32
|
+
id TEXT PRIMARY KEY,
|
|
33
|
+
campaign_id TEXT NOT NULL REFERENCES campaigns(id) ON DELETE CASCADE,
|
|
34
|
+
name TEXT NOT NULL,
|
|
35
|
+
targeting TEXT NOT NULL DEFAULT '{}',
|
|
36
|
+
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'active', 'paused', 'completed')),
|
|
37
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE TABLE IF NOT EXISTS ads (
|
|
41
|
+
id TEXT PRIMARY KEY,
|
|
42
|
+
ad_group_id TEXT NOT NULL REFERENCES ad_groups(id) ON DELETE CASCADE,
|
|
43
|
+
headline TEXT NOT NULL,
|
|
44
|
+
description TEXT,
|
|
45
|
+
creative_url TEXT,
|
|
46
|
+
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'active', 'paused', 'completed')),
|
|
47
|
+
metrics TEXT NOT NULL DEFAULT '{}',
|
|
48
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_campaigns_platform ON campaigns(platform);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_campaigns_status ON campaigns(status);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_campaigns_name ON campaigns(name);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_ad_groups_campaign ON ad_groups(campaign_id);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_ad_groups_status ON ad_groups(status);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_ads_ad_group ON ads(ad_group_id);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_ads_status ON ads(status);
|
|
58
|
+
`,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* microservice-ads — Ad campaign management microservice
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
createCampaign,
|
|
7
|
+
getCampaign,
|
|
8
|
+
listCampaigns,
|
|
9
|
+
updateCampaign,
|
|
10
|
+
deleteCampaign,
|
|
11
|
+
pauseCampaign,
|
|
12
|
+
resumeCampaign,
|
|
13
|
+
countCampaigns,
|
|
14
|
+
getCampaignStats,
|
|
15
|
+
getSpendByPlatform,
|
|
16
|
+
getPlatforms,
|
|
17
|
+
createAdGroup,
|
|
18
|
+
getAdGroup,
|
|
19
|
+
listAdGroups,
|
|
20
|
+
deleteAdGroup,
|
|
21
|
+
createAd,
|
|
22
|
+
getAd,
|
|
23
|
+
listAds,
|
|
24
|
+
deleteAd,
|
|
25
|
+
type Campaign,
|
|
26
|
+
type CreateCampaignInput,
|
|
27
|
+
type UpdateCampaignInput,
|
|
28
|
+
type ListCampaignsOptions,
|
|
29
|
+
type Platform,
|
|
30
|
+
type CampaignStatus,
|
|
31
|
+
type AdGroup,
|
|
32
|
+
type CreateAdGroupInput,
|
|
33
|
+
type Ad,
|
|
34
|
+
type CreateAdInput,
|
|
35
|
+
type CampaignStats,
|
|
36
|
+
type SpendByPlatform,
|
|
37
|
+
} from "./db/campaigns.js";
|
|
38
|
+
|
|
39
|
+
export { getDatabase, closeDatabase } from "./db/database.js";
|