Skip to content

Commit b77e016

Browse files
authored
Merge pull request #250 from objectstack-ai/copilot/update-data-storage-to-objectql
2 parents 6e0b470 + c058d5e commit b77e016

File tree

18 files changed

+1053
-8
lines changed

18 files changed

+1053
-8
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: audit_log
2+
label: Audit Log
3+
icon: history
4+
hidden: false
5+
fields:
6+
event_type:
7+
type: select
8+
label: Event Type
9+
required: true
10+
options:
11+
- label: Create
12+
value: create
13+
- label: Update
14+
value: update
15+
- label: Delete
16+
value: delete
17+
- label: Read
18+
value: read
19+
- label: Login
20+
value: login
21+
- label: Logout
22+
value: logout
23+
- label: Permission Change
24+
value: permission_change
25+
- label: Custom
26+
value: custom
27+
index: true
28+
object_name:
29+
type: text
30+
label: Object Name
31+
index: true
32+
record_id:
33+
type: text
34+
label: Record ID
35+
index: true
36+
user_id:
37+
type: text
38+
label: User ID
39+
index: true
40+
timestamp:
41+
type: datetime
42+
label: Timestamp
43+
required: true
44+
index: true
45+
ip_address:
46+
type: text
47+
label: IP Address
48+
user_agent:
49+
type: text
50+
label: User Agent
51+
session_id:
52+
type: text
53+
label: Session ID
54+
index: true
55+
changes:
56+
type: object
57+
label: Field Changes
58+
blackbox: true
59+
metadata:
60+
type: object
61+
label: Additional Metadata
62+
blackbox: true
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/**
2+
* ObjectQL Audit Storage Implementation
3+
*
4+
* Storage adapter that persists audit events to ObjectOS/ObjectQL database
5+
*/
6+
7+
import type { PluginContext } from '@objectstack/runtime';
8+
import type {
9+
AuditStorage,
10+
AuditLogEntry,
11+
AuditQueryOptions,
12+
FieldChange,
13+
AuditTrailEntry,
14+
} from './types.js';
15+
16+
export class ObjectQLAuditStorage implements AuditStorage {
17+
private context: PluginContext;
18+
19+
constructor(context: PluginContext) {
20+
this.context = context;
21+
}
22+
23+
/**
24+
* Store an audit event
25+
*/
26+
async logEvent(entry: AuditLogEntry): Promise<void> {
27+
await (this.context as any).broker.call('data.create', {
28+
object: 'audit_log',
29+
doc: {
30+
event_type: entry.eventType,
31+
object_name: (entry as any).objectName,
32+
record_id: (entry as any).recordId,
33+
user_id: entry.userId,
34+
timestamp: entry.timestamp || new Date().toISOString(),
35+
ip_address: entry.ipAddress,
36+
user_agent: entry.userAgent,
37+
session_id: entry.sessionId,
38+
changes: (entry as any).changes,
39+
metadata: entry.metadata,
40+
}
41+
});
42+
}
43+
44+
/**
45+
* Query audit events with filtering and pagination
46+
*/
47+
async queryEvents(options: AuditQueryOptions = {}): Promise<AuditLogEntry[]> {
48+
const query: any = {};
49+
50+
// Apply filters
51+
if (options.objectName) {
52+
query.object_name = options.objectName;
53+
}
54+
if (options.recordId) {
55+
query.record_id = options.recordId;
56+
}
57+
if (options.userId) {
58+
query.user_id = options.userId;
59+
}
60+
if (options.eventType) {
61+
query.event_type = options.eventType;
62+
}
63+
if (options.startDate) {
64+
query.timestamp = { $gte: options.startDate };
65+
}
66+
if (options.endDate) {
67+
if (query.timestamp) {
68+
query.timestamp.$lte = options.endDate;
69+
} else {
70+
query.timestamp = { $lte: options.endDate };
71+
}
72+
}
73+
74+
// Sort
75+
const sortOrder = options.sortOrder || 'desc';
76+
const sort = sortOrder === 'asc' ? 'timestamp' : '-timestamp';
77+
78+
// Query
79+
const results = await (this.context as any).broker.call('data.find', {
80+
object: 'audit_log',
81+
query: query,
82+
sort: sort,
83+
limit: options.limit,
84+
skip: options.offset,
85+
});
86+
87+
return results.map((doc: any) => this.mapDocToAuditEntry(doc));
88+
}
89+
90+
/**
91+
* Get field history for a specific record and field
92+
*/
93+
async getFieldHistory(
94+
objectName: string,
95+
recordId: string,
96+
fieldName: string
97+
): Promise<FieldChange[]> {
98+
const auditTrail = await this.getAuditTrail(objectName, recordId);
99+
const fieldChanges: FieldChange[] = [];
100+
101+
// Reverse to get chronological order (oldest first)
102+
const chronologicalTrail = [...auditTrail].reverse();
103+
104+
for (const entry of chronologicalTrail) {
105+
if (entry.changes) {
106+
const change = entry.changes.find(c => c.field === fieldName);
107+
if (change) {
108+
fieldChanges.push(change);
109+
}
110+
}
111+
}
112+
113+
return fieldChanges;
114+
}
115+
116+
/**
117+
* Get audit trail for a specific record
118+
*/
119+
async getAuditTrail(
120+
objectName: string,
121+
recordId: string
122+
): Promise<AuditTrailEntry[]> {
123+
const events = await this.queryEvents({ objectName, recordId });
124+
return events as AuditTrailEntry[];
125+
}
126+
127+
/**
128+
* Delete events with timestamp before the given date
129+
* Optionally filter by event type
130+
*/
131+
async deleteExpiredEvents(before: string, eventType?: string): Promise<number> {
132+
const query: any = {
133+
timestamp: { $lt: before }
134+
};
135+
136+
if (eventType) {
137+
query.event_type = eventType;
138+
}
139+
140+
const toDelete = await (this.context as any).broker.call('data.find', {
141+
object: 'audit_log',
142+
query: query,
143+
});
144+
145+
for (const doc of toDelete) {
146+
await (this.context as any).broker.call('data.delete', {
147+
object: 'audit_log',
148+
id: doc._id || doc.id,
149+
});
150+
}
151+
152+
return toDelete.length;
153+
}
154+
155+
/**
156+
* Map document to AuditLogEntry
157+
*/
158+
private mapDocToAuditEntry(doc: any): AuditLogEntry {
159+
const base = {
160+
eventType: doc.event_type,
161+
userId: doc.user_id,
162+
timestamp: doc.timestamp,
163+
ipAddress: doc.ip_address,
164+
userAgent: doc.user_agent,
165+
sessionId: doc.session_id,
166+
metadata: doc.metadata,
167+
};
168+
169+
// Add objectName and recordId if present (for AuditTrailEntry)
170+
if (doc.object_name) {
171+
(base as any).objectName = doc.object_name;
172+
}
173+
if (doc.record_id) {
174+
(base as any).recordId = doc.record_id;
175+
}
176+
if (doc.changes) {
177+
(base as any).changes = doc.changes;
178+
}
179+
180+
return base as AuditLogEntry;
181+
}
182+
}

packages/audit/src/plugin.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type {
2727
PluginStartupResult,
2828
} from './types.js';
2929
import { InMemoryAuditStorage } from './storage.js';
30+
import { ObjectQLAuditStorage } from './objectql-storage.js';
3031

3132
/**
3233
* Audit Log Plugin
@@ -61,6 +62,13 @@ export class AuditLogPlugin implements Plugin {
6162
this.context = context;
6263
this.startedAt = Date.now();
6364

65+
// Upgrade storage to ObjectQL if not explicitly provided and broker is available
66+
// We do this in init because we need the context
67+
if (!this.config.storage && (context as any).broker) {
68+
this.storage = new ObjectQLAuditStorage(context);
69+
context.logger.info('[Audit Log] Upgraded to ObjectQL storage');
70+
}
71+
6472
// Register audit log service
6573
context.registerService('audit-log', this);
6674

packages/automation/objects/automation_rule.object.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ fields:
1515
type: object
1616
label: Trigger Configuration
1717
blackbox: true
18-
conditions:
19-
type: object
20-
label: Conditions
21-
blackbox: true
2218
actions:
2319
type: object
2420
label: Actions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: formula_field
2+
label: Formula Field
3+
icon: calculator
4+
hidden: true
5+
fields:
6+
object_name:
7+
type: text
8+
required: true
9+
label: Object Name
10+
index: true
11+
name:
12+
type: text
13+
required: true
14+
label: Field Name
15+
formula:
16+
type: object
17+
label: Formula Definition
18+
blackbox: true

0 commit comments

Comments
 (0)