@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,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract, Clause, and Reminder CRUD operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getDatabase } from "./database.js";
|
|
6
|
+
|
|
7
|
+
// --- Contract types ---
|
|
8
|
+
|
|
9
|
+
export type ContractType = "nda" | "service" | "employment" | "license" | "other";
|
|
10
|
+
export type ContractStatus = "draft" | "pending_signature" | "active" | "expired" | "terminated";
|
|
11
|
+
|
|
12
|
+
export interface Contract {
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
type: ContractType;
|
|
16
|
+
status: ContractStatus;
|
|
17
|
+
counterparty: string | null;
|
|
18
|
+
counterparty_email: string | null;
|
|
19
|
+
start_date: string | null;
|
|
20
|
+
end_date: string | null;
|
|
21
|
+
auto_renew: boolean;
|
|
22
|
+
renewal_period: string | null;
|
|
23
|
+
value: number | null;
|
|
24
|
+
currency: string;
|
|
25
|
+
file_path: string | null;
|
|
26
|
+
metadata: Record<string, unknown>;
|
|
27
|
+
created_at: string;
|
|
28
|
+
updated_at: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ContractRow {
|
|
32
|
+
id: string;
|
|
33
|
+
title: string;
|
|
34
|
+
type: string;
|
|
35
|
+
status: string;
|
|
36
|
+
counterparty: string | null;
|
|
37
|
+
counterparty_email: string | null;
|
|
38
|
+
start_date: string | null;
|
|
39
|
+
end_date: string | null;
|
|
40
|
+
auto_renew: number;
|
|
41
|
+
renewal_period: string | null;
|
|
42
|
+
value: number | null;
|
|
43
|
+
currency: string;
|
|
44
|
+
file_path: string | null;
|
|
45
|
+
metadata: string;
|
|
46
|
+
created_at: string;
|
|
47
|
+
updated_at: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function rowToContract(row: ContractRow): Contract {
|
|
51
|
+
return {
|
|
52
|
+
...row,
|
|
53
|
+
type: row.type as ContractType,
|
|
54
|
+
status: row.status as ContractStatus,
|
|
55
|
+
auto_renew: row.auto_renew === 1,
|
|
56
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CreateContractInput {
|
|
61
|
+
title: string;
|
|
62
|
+
type?: ContractType;
|
|
63
|
+
status?: ContractStatus;
|
|
64
|
+
counterparty?: string;
|
|
65
|
+
counterparty_email?: string;
|
|
66
|
+
start_date?: string;
|
|
67
|
+
end_date?: string;
|
|
68
|
+
auto_renew?: boolean;
|
|
69
|
+
renewal_period?: string;
|
|
70
|
+
value?: number;
|
|
71
|
+
currency?: string;
|
|
72
|
+
file_path?: string;
|
|
73
|
+
metadata?: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createContract(input: CreateContractInput): Contract {
|
|
77
|
+
const db = getDatabase();
|
|
78
|
+
const id = crypto.randomUUID();
|
|
79
|
+
const metadata = JSON.stringify(input.metadata || {});
|
|
80
|
+
|
|
81
|
+
db.prepare(
|
|
82
|
+
`INSERT INTO contracts (id, title, type, status, counterparty, counterparty_email, start_date, end_date, auto_renew, renewal_period, value, currency, file_path, metadata)
|
|
83
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
84
|
+
).run(
|
|
85
|
+
id,
|
|
86
|
+
input.title,
|
|
87
|
+
input.type || "other",
|
|
88
|
+
input.status || "draft",
|
|
89
|
+
input.counterparty || null,
|
|
90
|
+
input.counterparty_email || null,
|
|
91
|
+
input.start_date || null,
|
|
92
|
+
input.end_date || null,
|
|
93
|
+
input.auto_renew ? 1 : 0,
|
|
94
|
+
input.renewal_period || null,
|
|
95
|
+
input.value ?? null,
|
|
96
|
+
input.currency || "USD",
|
|
97
|
+
input.file_path || null,
|
|
98
|
+
metadata
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return getContract(id)!;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getContract(id: string): Contract | null {
|
|
105
|
+
const db = getDatabase();
|
|
106
|
+
const row = db.prepare("SELECT * FROM contracts WHERE id = ?").get(id) as ContractRow | null;
|
|
107
|
+
return row ? rowToContract(row) : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface ListContractsOptions {
|
|
111
|
+
search?: string;
|
|
112
|
+
type?: ContractType;
|
|
113
|
+
status?: ContractStatus;
|
|
114
|
+
counterparty?: string;
|
|
115
|
+
limit?: number;
|
|
116
|
+
offset?: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function listContracts(options: ListContractsOptions = {}): Contract[] {
|
|
120
|
+
const db = getDatabase();
|
|
121
|
+
const conditions: string[] = [];
|
|
122
|
+
const params: unknown[] = [];
|
|
123
|
+
|
|
124
|
+
if (options.search) {
|
|
125
|
+
conditions.push(
|
|
126
|
+
"(title LIKE ? OR counterparty LIKE ? OR counterparty_email LIKE ?)"
|
|
127
|
+
);
|
|
128
|
+
const q = `%${options.search}%`;
|
|
129
|
+
params.push(q, q, q);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (options.type) {
|
|
133
|
+
conditions.push("type = ?");
|
|
134
|
+
params.push(options.type);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (options.status) {
|
|
138
|
+
conditions.push("status = ?");
|
|
139
|
+
params.push(options.status);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (options.counterparty) {
|
|
143
|
+
conditions.push("counterparty LIKE ?");
|
|
144
|
+
params.push(`%${options.counterparty}%`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let sql = "SELECT * FROM contracts";
|
|
148
|
+
if (conditions.length > 0) {
|
|
149
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
150
|
+
}
|
|
151
|
+
sql += " ORDER BY created_at DESC";
|
|
152
|
+
|
|
153
|
+
if (options.limit) {
|
|
154
|
+
sql += " LIMIT ?";
|
|
155
|
+
params.push(options.limit);
|
|
156
|
+
}
|
|
157
|
+
if (options.offset) {
|
|
158
|
+
sql += " OFFSET ?";
|
|
159
|
+
params.push(options.offset);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const rows = db.prepare(sql).all(...params) as ContractRow[];
|
|
163
|
+
return rows.map(rowToContract);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface UpdateContractInput {
|
|
167
|
+
title?: string;
|
|
168
|
+
type?: ContractType;
|
|
169
|
+
status?: ContractStatus;
|
|
170
|
+
counterparty?: string;
|
|
171
|
+
counterparty_email?: string;
|
|
172
|
+
start_date?: string;
|
|
173
|
+
end_date?: string;
|
|
174
|
+
auto_renew?: boolean;
|
|
175
|
+
renewal_period?: string;
|
|
176
|
+
value?: number;
|
|
177
|
+
currency?: string;
|
|
178
|
+
file_path?: string;
|
|
179
|
+
metadata?: Record<string, unknown>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function updateContract(
|
|
183
|
+
id: string,
|
|
184
|
+
input: UpdateContractInput
|
|
185
|
+
): Contract | null {
|
|
186
|
+
const db = getDatabase();
|
|
187
|
+
const existing = getContract(id);
|
|
188
|
+
if (!existing) return null;
|
|
189
|
+
|
|
190
|
+
const sets: string[] = [];
|
|
191
|
+
const params: unknown[] = [];
|
|
192
|
+
|
|
193
|
+
if (input.title !== undefined) {
|
|
194
|
+
sets.push("title = ?");
|
|
195
|
+
params.push(input.title);
|
|
196
|
+
}
|
|
197
|
+
if (input.type !== undefined) {
|
|
198
|
+
sets.push("type = ?");
|
|
199
|
+
params.push(input.type);
|
|
200
|
+
}
|
|
201
|
+
if (input.status !== undefined) {
|
|
202
|
+
sets.push("status = ?");
|
|
203
|
+
params.push(input.status);
|
|
204
|
+
}
|
|
205
|
+
if (input.counterparty !== undefined) {
|
|
206
|
+
sets.push("counterparty = ?");
|
|
207
|
+
params.push(input.counterparty);
|
|
208
|
+
}
|
|
209
|
+
if (input.counterparty_email !== undefined) {
|
|
210
|
+
sets.push("counterparty_email = ?");
|
|
211
|
+
params.push(input.counterparty_email);
|
|
212
|
+
}
|
|
213
|
+
if (input.start_date !== undefined) {
|
|
214
|
+
sets.push("start_date = ?");
|
|
215
|
+
params.push(input.start_date);
|
|
216
|
+
}
|
|
217
|
+
if (input.end_date !== undefined) {
|
|
218
|
+
sets.push("end_date = ?");
|
|
219
|
+
params.push(input.end_date);
|
|
220
|
+
}
|
|
221
|
+
if (input.auto_renew !== undefined) {
|
|
222
|
+
sets.push("auto_renew = ?");
|
|
223
|
+
params.push(input.auto_renew ? 1 : 0);
|
|
224
|
+
}
|
|
225
|
+
if (input.renewal_period !== undefined) {
|
|
226
|
+
sets.push("renewal_period = ?");
|
|
227
|
+
params.push(input.renewal_period);
|
|
228
|
+
}
|
|
229
|
+
if (input.value !== undefined) {
|
|
230
|
+
sets.push("value = ?");
|
|
231
|
+
params.push(input.value);
|
|
232
|
+
}
|
|
233
|
+
if (input.currency !== undefined) {
|
|
234
|
+
sets.push("currency = ?");
|
|
235
|
+
params.push(input.currency);
|
|
236
|
+
}
|
|
237
|
+
if (input.file_path !== undefined) {
|
|
238
|
+
sets.push("file_path = ?");
|
|
239
|
+
params.push(input.file_path);
|
|
240
|
+
}
|
|
241
|
+
if (input.metadata !== undefined) {
|
|
242
|
+
sets.push("metadata = ?");
|
|
243
|
+
params.push(JSON.stringify(input.metadata));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (sets.length === 0) return existing;
|
|
247
|
+
|
|
248
|
+
sets.push("updated_at = datetime('now')");
|
|
249
|
+
params.push(id);
|
|
250
|
+
|
|
251
|
+
db.prepare(
|
|
252
|
+
`UPDATE contracts SET ${sets.join(", ")} WHERE id = ?`
|
|
253
|
+
).run(...params);
|
|
254
|
+
|
|
255
|
+
return getContract(id);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function deleteContract(id: string): boolean {
|
|
259
|
+
const db = getDatabase();
|
|
260
|
+
const result = db.prepare("DELETE FROM contracts WHERE id = ?").run(id);
|
|
261
|
+
return result.changes > 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function searchContracts(query: string): Contract[] {
|
|
265
|
+
return listContracts({ search: query });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* List contracts expiring within the given number of days
|
|
270
|
+
*/
|
|
271
|
+
export function listExpiring(days: number): Contract[] {
|
|
272
|
+
const db = getDatabase();
|
|
273
|
+
const rows = db
|
|
274
|
+
.prepare(
|
|
275
|
+
`SELECT * FROM contracts
|
|
276
|
+
WHERE end_date IS NOT NULL
|
|
277
|
+
AND status IN ('active', 'pending_signature')
|
|
278
|
+
AND date(end_date) <= date('now', '+' || ? || ' days')
|
|
279
|
+
AND date(end_date) >= date('now')
|
|
280
|
+
ORDER BY end_date ASC`
|
|
281
|
+
)
|
|
282
|
+
.all(days) as ContractRow[];
|
|
283
|
+
return rows.map(rowToContract);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Renew a contract by extending its end_date based on renewal_period.
|
|
288
|
+
* If renewal_period is not set, extends by 1 year.
|
|
289
|
+
*/
|
|
290
|
+
export function renewContract(id: string): Contract | null {
|
|
291
|
+
const contract = getContract(id);
|
|
292
|
+
if (!contract) return null;
|
|
293
|
+
|
|
294
|
+
const period = contract.renewal_period || "1 year";
|
|
295
|
+
const baseDate = contract.end_date || new Date().toISOString().split("T")[0];
|
|
296
|
+
|
|
297
|
+
const db = getDatabase();
|
|
298
|
+
db.prepare(
|
|
299
|
+
`UPDATE contracts
|
|
300
|
+
SET end_date = date(?, '+' || ?),
|
|
301
|
+
status = 'active',
|
|
302
|
+
updated_at = datetime('now')
|
|
303
|
+
WHERE id = ?`
|
|
304
|
+
).run(baseDate, period, id);
|
|
305
|
+
|
|
306
|
+
return getContract(id);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get aggregate stats about contracts
|
|
311
|
+
*/
|
|
312
|
+
export function getContractStats(): {
|
|
313
|
+
total: number;
|
|
314
|
+
by_status: Record<string, number>;
|
|
315
|
+
by_type: Record<string, number>;
|
|
316
|
+
total_value: number;
|
|
317
|
+
expiring_30_days: number;
|
|
318
|
+
} {
|
|
319
|
+
const db = getDatabase();
|
|
320
|
+
|
|
321
|
+
const total = (
|
|
322
|
+
db.prepare("SELECT COUNT(*) as count FROM contracts").get() as { count: number }
|
|
323
|
+
).count;
|
|
324
|
+
|
|
325
|
+
const statusRows = db
|
|
326
|
+
.prepare("SELECT status, COUNT(*) as count FROM contracts GROUP BY status")
|
|
327
|
+
.all() as { status: string; count: number }[];
|
|
328
|
+
const by_status: Record<string, number> = {};
|
|
329
|
+
for (const row of statusRows) {
|
|
330
|
+
by_status[row.status] = row.count;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const typeRows = db
|
|
334
|
+
.prepare("SELECT type, COUNT(*) as count FROM contracts GROUP BY type")
|
|
335
|
+
.all() as { type: string; count: number }[];
|
|
336
|
+
const by_type: Record<string, number> = {};
|
|
337
|
+
for (const row of typeRows) {
|
|
338
|
+
by_type[row.type] = row.count;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const valueRow = db
|
|
342
|
+
.prepare("SELECT COALESCE(SUM(value), 0) as total FROM contracts WHERE status = 'active'")
|
|
343
|
+
.get() as { total: number };
|
|
344
|
+
|
|
345
|
+
const expiringRow = db
|
|
346
|
+
.prepare(
|
|
347
|
+
`SELECT COUNT(*) as count FROM contracts
|
|
348
|
+
WHERE end_date IS NOT NULL
|
|
349
|
+
AND status IN ('active', 'pending_signature')
|
|
350
|
+
AND date(end_date) <= date('now', '+30 days')
|
|
351
|
+
AND date(end_date) >= date('now')`
|
|
352
|
+
)
|
|
353
|
+
.get() as { count: number };
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
total,
|
|
357
|
+
by_status,
|
|
358
|
+
by_type,
|
|
359
|
+
total_value: valueRow.total,
|
|
360
|
+
expiring_30_days: expiringRow.count,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// --- Clause operations ---
|
|
365
|
+
|
|
366
|
+
export interface Clause {
|
|
367
|
+
id: string;
|
|
368
|
+
contract_id: string;
|
|
369
|
+
name: string;
|
|
370
|
+
text: string;
|
|
371
|
+
type: "standard" | "custom" | "negotiated";
|
|
372
|
+
created_at: string;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export interface CreateClauseInput {
|
|
376
|
+
contract_id: string;
|
|
377
|
+
name: string;
|
|
378
|
+
text: string;
|
|
379
|
+
type?: "standard" | "custom" | "negotiated";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function createClause(input: CreateClauseInput): Clause {
|
|
383
|
+
const db = getDatabase();
|
|
384
|
+
const id = crypto.randomUUID();
|
|
385
|
+
|
|
386
|
+
db.prepare(
|
|
387
|
+
`INSERT INTO clauses (id, contract_id, name, text, type)
|
|
388
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
389
|
+
).run(id, input.contract_id, input.name, input.text, input.type || "standard");
|
|
390
|
+
|
|
391
|
+
return getClause(id)!;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function getClause(id: string): Clause | null {
|
|
395
|
+
const db = getDatabase();
|
|
396
|
+
const row = db.prepare("SELECT * FROM clauses WHERE id = ?").get(id) as Clause | null;
|
|
397
|
+
return row || null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function listClauses(contractId: string): Clause[] {
|
|
401
|
+
const db = getDatabase();
|
|
402
|
+
return db
|
|
403
|
+
.prepare("SELECT * FROM clauses WHERE contract_id = ? ORDER BY created_at ASC")
|
|
404
|
+
.all(contractId) as Clause[];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function deleteClause(id: string): boolean {
|
|
408
|
+
const db = getDatabase();
|
|
409
|
+
const result = db.prepare("DELETE FROM clauses WHERE id = ?").run(id);
|
|
410
|
+
return result.changes > 0;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// --- Reminder operations ---
|
|
414
|
+
|
|
415
|
+
export interface Reminder {
|
|
416
|
+
id: string;
|
|
417
|
+
contract_id: string;
|
|
418
|
+
remind_at: string;
|
|
419
|
+
message: string;
|
|
420
|
+
sent: boolean;
|
|
421
|
+
created_at: string;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
interface ReminderRow {
|
|
425
|
+
id: string;
|
|
426
|
+
contract_id: string;
|
|
427
|
+
remind_at: string;
|
|
428
|
+
message: string;
|
|
429
|
+
sent: number;
|
|
430
|
+
created_at: string;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function rowToReminder(row: ReminderRow): Reminder {
|
|
434
|
+
return {
|
|
435
|
+
...row,
|
|
436
|
+
sent: row.sent === 1,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export interface CreateReminderInput {
|
|
441
|
+
contract_id: string;
|
|
442
|
+
remind_at: string;
|
|
443
|
+
message: string;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function createReminder(input: CreateReminderInput): Reminder {
|
|
447
|
+
const db = getDatabase();
|
|
448
|
+
const id = crypto.randomUUID();
|
|
449
|
+
|
|
450
|
+
db.prepare(
|
|
451
|
+
`INSERT INTO reminders (id, contract_id, remind_at, message)
|
|
452
|
+
VALUES (?, ?, ?, ?)`
|
|
453
|
+
).run(id, input.contract_id, input.remind_at, input.message);
|
|
454
|
+
|
|
455
|
+
return getReminder(id)!;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function getReminder(id: string): Reminder | null {
|
|
459
|
+
const db = getDatabase();
|
|
460
|
+
const row = db.prepare("SELECT * FROM reminders WHERE id = ?").get(id) as ReminderRow | null;
|
|
461
|
+
return row ? rowToReminder(row) : null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function listReminders(contractId: string): Reminder[] {
|
|
465
|
+
const db = getDatabase();
|
|
466
|
+
const rows = db
|
|
467
|
+
.prepare("SELECT * FROM reminders WHERE contract_id = ? ORDER BY remind_at ASC")
|
|
468
|
+
.all(contractId) as ReminderRow[];
|
|
469
|
+
return rows.map(rowToReminder);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function deleteReminder(id: string): boolean {
|
|
473
|
+
const db = getDatabase();
|
|
474
|
+
const result = db.prepare("DELETE FROM reminders WHERE id = ?").run(id);
|
|
475
|
+
return result.changes > 0;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function listPendingReminders(): Reminder[] {
|
|
479
|
+
const db = getDatabase();
|
|
480
|
+
const rows = db
|
|
481
|
+
.prepare(
|
|
482
|
+
`SELECT * FROM reminders
|
|
483
|
+
WHERE sent = 0 AND datetime(remind_at) <= datetime('now')
|
|
484
|
+
ORDER BY remind_at ASC`
|
|
485
|
+
)
|
|
486
|
+
.all() as ReminderRow[];
|
|
487
|
+
return rows.map(rowToReminder);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function markReminderSent(id: string): boolean {
|
|
491
|
+
const db = getDatabase();
|
|
492
|
+
const result = db
|
|
493
|
+
.prepare("UPDATE reminders SET sent = 1 WHERE id = ?")
|
|
494
|
+
.run(id);
|
|
495
|
+
return result.changes > 0;
|
|
496
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database connection for microservice-contracts
|
|
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-contracts", "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-contracts", "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-contracts", "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,58 @@
|
|
|
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 contracts (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
title TEXT NOT NULL,
|
|
15
|
+
type TEXT NOT NULL DEFAULT 'other' CHECK (type IN ('nda', 'service', 'employment', 'license', 'other')),
|
|
16
|
+
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'pending_signature', 'active', 'expired', 'terminated')),
|
|
17
|
+
counterparty TEXT,
|
|
18
|
+
counterparty_email TEXT,
|
|
19
|
+
start_date TEXT,
|
|
20
|
+
end_date TEXT,
|
|
21
|
+
auto_renew INTEGER NOT NULL DEFAULT 0,
|
|
22
|
+
renewal_period TEXT,
|
|
23
|
+
value REAL,
|
|
24
|
+
currency TEXT NOT NULL DEFAULT 'USD',
|
|
25
|
+
file_path TEXT,
|
|
26
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
27
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
28
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS clauses (
|
|
32
|
+
id TEXT PRIMARY KEY,
|
|
33
|
+
contract_id TEXT NOT NULL REFERENCES contracts(id) ON DELETE CASCADE,
|
|
34
|
+
name TEXT NOT NULL,
|
|
35
|
+
text TEXT NOT NULL,
|
|
36
|
+
type TEXT NOT NULL DEFAULT 'standard' CHECK (type IN ('standard', 'custom', 'negotiated')),
|
|
37
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE TABLE IF NOT EXISTS reminders (
|
|
41
|
+
id TEXT PRIMARY KEY,
|
|
42
|
+
contract_id TEXT NOT NULL REFERENCES contracts(id) ON DELETE CASCADE,
|
|
43
|
+
remind_at TEXT NOT NULL,
|
|
44
|
+
message TEXT NOT NULL,
|
|
45
|
+
sent INTEGER NOT NULL DEFAULT 0,
|
|
46
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_contracts_status ON contracts(status);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_contracts_type ON contracts(type);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_contracts_counterparty ON contracts(counterparty);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_contracts_end_date ON contracts(end_date);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_clauses_contract ON clauses(contract_id);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_reminders_contract ON reminders(contract_id);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_reminders_remind_at ON reminders(remind_at);
|
|
56
|
+
`,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* microservice-contracts — Contract and agreement management microservice
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
createContract,
|
|
7
|
+
getContract,
|
|
8
|
+
listContracts,
|
|
9
|
+
updateContract,
|
|
10
|
+
deleteContract,
|
|
11
|
+
searchContracts,
|
|
12
|
+
listExpiring,
|
|
13
|
+
renewContract,
|
|
14
|
+
getContractStats,
|
|
15
|
+
type Contract,
|
|
16
|
+
type ContractType,
|
|
17
|
+
type ContractStatus,
|
|
18
|
+
type CreateContractInput,
|
|
19
|
+
type UpdateContractInput,
|
|
20
|
+
type ListContractsOptions,
|
|
21
|
+
} from "./db/contracts.js";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
createClause,
|
|
25
|
+
getClause,
|
|
26
|
+
listClauses,
|
|
27
|
+
deleteClause,
|
|
28
|
+
type Clause,
|
|
29
|
+
type CreateClauseInput,
|
|
30
|
+
} from "./db/contracts.js";
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
createReminder,
|
|
34
|
+
getReminder,
|
|
35
|
+
listReminders,
|
|
36
|
+
deleteReminder,
|
|
37
|
+
listPendingReminders,
|
|
38
|
+
markReminderSent,
|
|
39
|
+
type Reminder,
|
|
40
|
+
type CreateReminderInput,
|
|
41
|
+
} from "./db/contracts.js";
|
|
42
|
+
|
|
43
|
+
export { getDatabase, closeDatabase } from "./db/database.js";
|