@prmichaelsen/remember-core 0.12.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.
- package/README.md +100 -0
- package/dist/collections/composite-ids.d.ts +47 -0
- package/dist/collections/composite-ids.d.ts.map +1 -0
- package/dist/collections/composite-ids.js +97 -0
- package/dist/collections/composite-ids.js.map +1 -0
- package/dist/collections/dot-notation.d.ts +37 -0
- package/dist/collections/dot-notation.d.ts.map +1 -0
- package/dist/collections/dot-notation.js +100 -0
- package/dist/collections/dot-notation.js.map +1 -0
- package/dist/collections/index.d.ts +7 -0
- package/dist/collections/index.d.ts.map +1 -0
- package/dist/collections/index.js +7 -0
- package/dist/collections/index.js.map +1 -0
- package/dist/collections/tracking-arrays.d.ts +28 -0
- package/dist/collections/tracking-arrays.d.ts.map +1 -0
- package/dist/collections/tracking-arrays.js +67 -0
- package/dist/collections/tracking-arrays.js.map +1 -0
- package/dist/config/debug.d.ts +36 -0
- package/dist/config/debug.d.ts.map +1 -0
- package/dist/config/debug.js +48 -0
- package/dist/config/debug.js.map +1 -0
- package/dist/config/environment.d.ts +71 -0
- package/dist/config/environment.d.ts.map +1 -0
- package/dist/config/environment.js +57 -0
- package/dist/config/environment.js.map +1 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +27 -0
- package/dist/config/index.js.map +1 -0
- package/dist/constants/content-types.d.ts +60 -0
- package/dist/constants/content-types.d.ts.map +1 -0
- package/dist/constants/content-types.js +450 -0
- package/dist/constants/content-types.js.map +1 -0
- package/dist/constants/index.d.ts +3 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +3 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/database/firestore/index.d.ts +8 -0
- package/dist/database/firestore/index.d.ts.map +1 -0
- package/dist/database/firestore/index.js +10 -0
- package/dist/database/firestore/index.js.map +1 -0
- package/dist/database/firestore/init.d.ts +51 -0
- package/dist/database/firestore/init.d.ts.map +1 -0
- package/dist/database/firestore/init.js +92 -0
- package/dist/database/firestore/init.js.map +1 -0
- package/dist/database/firestore/paths.d.ts +54 -0
- package/dist/database/firestore/paths.d.ts.map +1 -0
- package/dist/database/firestore/paths.js +103 -0
- package/dist/database/firestore/paths.js.map +1 -0
- package/dist/database/weaviate/client.d.ts +68 -0
- package/dist/database/weaviate/client.d.ts.map +1 -0
- package/dist/database/weaviate/client.js +157 -0
- package/dist/database/weaviate/client.js.map +1 -0
- package/dist/database/weaviate/index.d.ts +6 -0
- package/dist/database/weaviate/index.d.ts.map +1 -0
- package/dist/database/weaviate/index.js +9 -0
- package/dist/database/weaviate/index.js.map +1 -0
- package/dist/database/weaviate/schema.d.ts +22 -0
- package/dist/database/weaviate/schema.d.ts.map +1 -0
- package/dist/database/weaviate/schema.js +50 -0
- package/dist/database/weaviate/schema.js.map +1 -0
- package/dist/database/weaviate/space-schema.d.ts +36 -0
- package/dist/database/weaviate/space-schema.d.ts.map +1 -0
- package/dist/database/weaviate/space-schema.js +65 -0
- package/dist/database/weaviate/space-schema.js.map +1 -0
- package/dist/database/weaviate/v2-collections.d.ts +160 -0
- package/dist/database/weaviate/v2-collections.d.ts.map +1 -0
- package/dist/database/weaviate/v2-collections.js +275 -0
- package/dist/database/weaviate/v2-collections.js.map +1 -0
- package/dist/errors/app-errors.d.ts +64 -0
- package/dist/errors/app-errors.d.ts.map +1 -0
- package/dist/errors/app-errors.js +90 -0
- package/dist/errors/app-errors.js.map +1 -0
- package/dist/errors/base.error.d.ts +15 -0
- package/dist/errors/base.error.d.ts.map +1 -0
- package/dist/errors/base.error.js +25 -0
- package/dist/errors/base.error.js.map +1 -0
- package/dist/errors/index.d.ts +19 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +25 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/services/confirmation-token.service.d.ts +56 -0
- package/dist/services/confirmation-token.service.d.ts.map +1 -0
- package/dist/services/confirmation-token.service.js +118 -0
- package/dist/services/confirmation-token.service.js.map +1 -0
- package/dist/services/credentials-provider.d.ts +25 -0
- package/dist/services/credentials-provider.d.ts.map +1 -0
- package/dist/services/credentials-provider.js +31 -0
- package/dist/services/credentials-provider.js.map +1 -0
- package/dist/services/index.d.ts +11 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +17 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/memory.service.d.ts +129 -0
- package/dist/services/memory.service.d.ts.map +1 -0
- package/dist/services/memory.service.js +294 -0
- package/dist/services/memory.service.js.map +1 -0
- package/dist/services/preferences.service.d.ts +27 -0
- package/dist/services/preferences.service.d.ts.map +1 -0
- package/dist/services/preferences.service.js +106 -0
- package/dist/services/preferences.service.js.map +1 -0
- package/dist/services/relationship.service.d.ts +75 -0
- package/dist/services/relationship.service.d.ts.map +1 -0
- package/dist/services/relationship.service.js +211 -0
- package/dist/services/relationship.service.js.map +1 -0
- package/dist/services/space-config.service.d.ts +22 -0
- package/dist/services/space-config.service.d.ts.map +1 -0
- package/dist/services/space-config.service.js +50 -0
- package/dist/services/space-config.service.js.map +1 -0
- package/dist/services/space.service.d.ts +160 -0
- package/dist/services/space.service.d.ts.map +1 -0
- package/dist/services/space.service.js +815 -0
- package/dist/services/space.service.js.map +1 -0
- package/dist/testing/index.d.ts +8 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +8 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/test-data-generator.d.ts +100 -0
- package/dist/testing/test-data-generator.d.ts.map +1 -0
- package/dist/testing/test-data-generator.js +194 -0
- package/dist/testing/test-data-generator.js.map +1 -0
- package/dist/testing/weaviate-mock.d.ts +317 -0
- package/dist/testing/weaviate-mock.d.ts.map +1 -0
- package/dist/testing/weaviate-mock.js +233 -0
- package/dist/testing/weaviate-mock.js.map +1 -0
- package/dist/types/auth.types.d.ts +36 -0
- package/dist/types/auth.types.d.ts.map +1 -0
- package/dist/types/auth.types.js +9 -0
- package/dist/types/auth.types.js.map +1 -0
- package/dist/types/context.types.d.ts +78 -0
- package/dist/types/context.types.d.ts.map +1 -0
- package/dist/types/context.types.js +6 -0
- package/dist/types/context.types.js.map +1 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/llm.types.d.ts +8 -0
- package/dist/types/llm.types.d.ts.map +1 -0
- package/dist/types/llm.types.js +8 -0
- package/dist/types/llm.types.js.map +1 -0
- package/dist/types/memory.types.d.ts +83 -0
- package/dist/types/memory.types.d.ts.map +1 -0
- package/dist/types/memory.types.js +6 -0
- package/dist/types/memory.types.js.map +1 -0
- package/dist/types/preferences.types.d.ts +285 -0
- package/dist/types/preferences.types.d.ts.map +1 -0
- package/dist/types/preferences.types.js +195 -0
- package/dist/types/preferences.types.js.map +1 -0
- package/dist/types/result.types.d.ts +60 -0
- package/dist/types/result.types.d.ts.map +1 -0
- package/dist/types/result.types.js +82 -0
- package/dist/types/result.types.js.map +1 -0
- package/dist/types/search.types.d.ts +49 -0
- package/dist/types/search.types.d.ts.map +1 -0
- package/dist/types/search.types.js +6 -0
- package/dist/types/search.types.js.map +1 -0
- package/dist/types/shared.types.d.ts +91 -0
- package/dist/types/shared.types.d.ts.map +1 -0
- package/dist/types/shared.types.js +57 -0
- package/dist/types/shared.types.js.map +1 -0
- package/dist/types/space.types.d.ts +82 -0
- package/dist/types/space.types.d.ts.map +1 -0
- package/dist/types/space.types.js +18 -0
- package/dist/types/space.types.js.map +1 -0
- package/dist/types/utils.types.d.ts +51 -0
- package/dist/types/utils.types.d.ts.map +1 -0
- package/dist/types/utils.types.js +4 -0
- package/dist/types/utils.types.js.map +1 -0
- package/dist/utils/auth-helpers.d.ts +23 -0
- package/dist/utils/auth-helpers.d.ts.map +1 -0
- package/dist/utils/auth-helpers.js +31 -0
- package/dist/utils/auth-helpers.js.map +1 -0
- package/dist/utils/debug.d.ts +45 -0
- package/dist/utils/debug.d.ts.map +1 -0
- package/dist/utils/debug.js +112 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/error-handler.d.ts +46 -0
- package/dist/utils/error-handler.d.ts.map +1 -0
- package/dist/utils/error-handler.js +63 -0
- package/dist/utils/error-handler.js.map +1 -0
- package/dist/utils/filters.d.ts +42 -0
- package/dist/utils/filters.d.ts.map +1 -0
- package/dist/utils/filters.js +132 -0
- package/dist/utils/filters.js.map +1 -0
- package/dist/utils/index.d.ts +9 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +22 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +68 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +100 -0
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpaceService — publish/retract/revise/confirm/deny/moderate/search/query.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from 8 remember-mcp tool handlers:
|
|
5
|
+
* publish.ts, retract.ts, revise.ts, confirm.ts,
|
|
6
|
+
* deny.ts, moderate.ts, search-space.ts, query-space.ts
|
|
7
|
+
*
|
|
8
|
+
* Design: accepts Weaviate client + user collection + ConfirmationTokenService + Logger via constructor.
|
|
9
|
+
* No MCP-specific code.
|
|
10
|
+
*/
|
|
11
|
+
import { Filters } from 'weaviate-client';
|
|
12
|
+
import { fetchMemoryWithAllProperties } from '../database/weaviate/client.js';
|
|
13
|
+
import { ensurePublicCollection, isValidSpaceId } from '../database/weaviate/space-schema.js';
|
|
14
|
+
import { CollectionType, getCollectionName } from '../collections/dot-notation.js';
|
|
15
|
+
import { generateCompositeId } from '../collections/composite-ids.js';
|
|
16
|
+
import { getSpaceConfig } from './space-config.service.js';
|
|
17
|
+
import { canModerate, canModerateAny } from '../utils/auth-helpers.js';
|
|
18
|
+
const ACTION_TO_STATUS = {
|
|
19
|
+
approve: 'approved',
|
|
20
|
+
reject: 'rejected',
|
|
21
|
+
remove: 'removed',
|
|
22
|
+
};
|
|
23
|
+
// ─── Revision History Helpers ───────────────────────────────────────────
|
|
24
|
+
const MAX_REVISION_HISTORY = 10;
|
|
25
|
+
export function parseRevisionHistory(raw) {
|
|
26
|
+
if (!raw || typeof raw !== 'string')
|
|
27
|
+
return [];
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
if (!Array.isArray(parsed))
|
|
31
|
+
return [];
|
|
32
|
+
return parsed.filter((e) => typeof e === 'object' &&
|
|
33
|
+
e !== null &&
|
|
34
|
+
typeof e.content === 'string' &&
|
|
35
|
+
typeof e.revised_at === 'string');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function buildRevisionHistory(existing, oldContent, revisedAt) {
|
|
42
|
+
const updated = [{ content: oldContent, revised_at: revisedAt }, ...existing];
|
|
43
|
+
return updated.slice(0, MAX_REVISION_HISTORY);
|
|
44
|
+
}
|
|
45
|
+
// ─── Filter Helpers ─────────────────────────────────────────────────────
|
|
46
|
+
export function buildModerationFilter(collection, moderationFilter = 'approved') {
|
|
47
|
+
if (moderationFilter === 'all')
|
|
48
|
+
return null;
|
|
49
|
+
if (moderationFilter === 'approved') {
|
|
50
|
+
return Filters.or(collection.filter.byProperty('moderation_status').equal('approved'), collection.filter.byProperty('moderation_status').isNull(true));
|
|
51
|
+
}
|
|
52
|
+
return collection.filter.byProperty('moderation_status').equal(moderationFilter);
|
|
53
|
+
}
|
|
54
|
+
// ─── Service ────────────────────────────────────────────────────────────
|
|
55
|
+
/**
|
|
56
|
+
* SpaceService provides transport-agnostic space operations.
|
|
57
|
+
*
|
|
58
|
+
* @param weaviateClient - Weaviate client instance (for multi-collection access)
|
|
59
|
+
* @param userCollection - User's personal Weaviate collection
|
|
60
|
+
* @param userId - The owner user ID
|
|
61
|
+
* @param confirmationTokenService - ConfirmationTokenService instance
|
|
62
|
+
* @param logger - Logger instance
|
|
63
|
+
*/
|
|
64
|
+
export class SpaceService {
|
|
65
|
+
weaviateClient;
|
|
66
|
+
userCollection;
|
|
67
|
+
userId;
|
|
68
|
+
confirmationTokenService;
|
|
69
|
+
logger;
|
|
70
|
+
constructor(weaviateClient, userCollection, userId, confirmationTokenService, logger) {
|
|
71
|
+
this.weaviateClient = weaviateClient;
|
|
72
|
+
this.userCollection = userCollection;
|
|
73
|
+
this.userId = userId;
|
|
74
|
+
this.confirmationTokenService = confirmationTokenService;
|
|
75
|
+
this.logger = logger;
|
|
76
|
+
}
|
|
77
|
+
// ── Publish (phase 1: generate confirmation token) ──────────────────
|
|
78
|
+
async publish(input) {
|
|
79
|
+
const spaces = input.spaces || [];
|
|
80
|
+
const groups = input.groups || [];
|
|
81
|
+
if (spaces.length === 0 && groups.length === 0) {
|
|
82
|
+
throw new Error('Must specify at least one space or group to publish to');
|
|
83
|
+
}
|
|
84
|
+
// Validate space IDs
|
|
85
|
+
if (spaces.length > 0) {
|
|
86
|
+
const invalidSpaces = spaces.filter((s) => !isValidSpaceId(s));
|
|
87
|
+
if (invalidSpaces.length > 0) {
|
|
88
|
+
throw new Error(`Invalid space IDs: ${invalidSpaces.join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Validate group IDs (no dots, not empty)
|
|
92
|
+
if (groups.length > 0) {
|
|
93
|
+
const invalidGroups = groups.filter((g) => !g || g.includes('.') || g.trim() === '');
|
|
94
|
+
if (invalidGroups.length > 0) {
|
|
95
|
+
throw new Error('Group IDs cannot be empty or contain dots');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Verify memory exists, belongs to user, is a memory
|
|
99
|
+
const memory = await fetchMemoryWithAllProperties(this.userCollection, input.memory_id);
|
|
100
|
+
if (!memory)
|
|
101
|
+
throw new Error(`Memory not found: ${input.memory_id}`);
|
|
102
|
+
if (memory.properties.user_id !== this.userId)
|
|
103
|
+
throw new Error('Permission denied: not memory owner');
|
|
104
|
+
if (memory.properties.doc_type !== 'memory')
|
|
105
|
+
throw new Error('Only memories can be published');
|
|
106
|
+
// Generate confirmation token
|
|
107
|
+
const { token } = await this.confirmationTokenService.createRequest(this.userId, 'publish_memory', {
|
|
108
|
+
memory_id: input.memory_id,
|
|
109
|
+
spaces,
|
|
110
|
+
groups,
|
|
111
|
+
additional_tags: input.additional_tags || [],
|
|
112
|
+
});
|
|
113
|
+
this.logger.info('Publish confirmation created', {
|
|
114
|
+
userId: this.userId,
|
|
115
|
+
memoryId: input.memory_id,
|
|
116
|
+
spaces,
|
|
117
|
+
groups,
|
|
118
|
+
});
|
|
119
|
+
return { token };
|
|
120
|
+
}
|
|
121
|
+
// ── Retract (phase 1: generate confirmation token) ──────────────────
|
|
122
|
+
async retract(input) {
|
|
123
|
+
const spaces = input.spaces || [];
|
|
124
|
+
const groups = input.groups || [];
|
|
125
|
+
if (spaces.length === 0 && groups.length === 0) {
|
|
126
|
+
throw new Error('Must specify at least one space or group to retract from');
|
|
127
|
+
}
|
|
128
|
+
// Validate group IDs
|
|
129
|
+
if (groups.length > 0) {
|
|
130
|
+
const invalidGroups = groups.filter((g) => g.includes('.'));
|
|
131
|
+
if (invalidGroups.length > 0) {
|
|
132
|
+
throw new Error(`Group IDs cannot contain dots: ${invalidGroups.join(', ')}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Verify memory exists and belongs to user
|
|
136
|
+
const memory = await fetchMemoryWithAllProperties(this.userCollection, input.memory_id);
|
|
137
|
+
if (!memory)
|
|
138
|
+
throw new Error(`Memory not found: ${input.memory_id}`);
|
|
139
|
+
if (memory.properties.user_id !== this.userId)
|
|
140
|
+
throw new Error('Permission denied: not memory owner');
|
|
141
|
+
// Check current publication status
|
|
142
|
+
const currentSpaceIds = Array.isArray(memory.properties.space_ids)
|
|
143
|
+
? memory.properties.space_ids
|
|
144
|
+
: [];
|
|
145
|
+
const currentGroupIds = Array.isArray(memory.properties.group_ids)
|
|
146
|
+
? memory.properties.group_ids
|
|
147
|
+
: [];
|
|
148
|
+
const notPublishedSpaces = spaces.filter((s) => !currentSpaceIds.includes(s));
|
|
149
|
+
const notPublishedGroups = groups.filter((g) => !currentGroupIds.includes(g));
|
|
150
|
+
if (notPublishedSpaces.length > 0 || notPublishedGroups.length > 0) {
|
|
151
|
+
throw new Error(`Memory is not published to some destinations. ` +
|
|
152
|
+
`Not in spaces: [${notPublishedSpaces.join(', ')}], ` +
|
|
153
|
+
`Not in groups: [${notPublishedGroups.join(', ')}]`);
|
|
154
|
+
}
|
|
155
|
+
// Generate confirmation token
|
|
156
|
+
const { token } = await this.confirmationTokenService.createRequest(this.userId, 'retract_memory', {
|
|
157
|
+
memory_id: input.memory_id,
|
|
158
|
+
spaces,
|
|
159
|
+
groups,
|
|
160
|
+
current_space_ids: currentSpaceIds,
|
|
161
|
+
current_group_ids: currentGroupIds,
|
|
162
|
+
});
|
|
163
|
+
this.logger.info('Retract confirmation created', {
|
|
164
|
+
userId: this.userId,
|
|
165
|
+
memoryId: input.memory_id,
|
|
166
|
+
spaces,
|
|
167
|
+
groups,
|
|
168
|
+
});
|
|
169
|
+
return { token };
|
|
170
|
+
}
|
|
171
|
+
// ── Revise (phase 1: generate confirmation token) ───────────────────
|
|
172
|
+
async revise(input) {
|
|
173
|
+
const memory = await fetchMemoryWithAllProperties(this.userCollection, input.memory_id);
|
|
174
|
+
if (!memory)
|
|
175
|
+
throw new Error(`Memory not found: ${input.memory_id}`);
|
|
176
|
+
if (memory.properties.user_id !== this.userId)
|
|
177
|
+
throw new Error('Permission denied: not memory owner');
|
|
178
|
+
const spaceIds = Array.isArray(memory.properties.space_ids)
|
|
179
|
+
? memory.properties.space_ids
|
|
180
|
+
: [];
|
|
181
|
+
const groupIds = Array.isArray(memory.properties.group_ids)
|
|
182
|
+
? memory.properties.group_ids
|
|
183
|
+
: [];
|
|
184
|
+
if (spaceIds.length === 0 && groupIds.length === 0) {
|
|
185
|
+
throw new Error('Memory has no published copies to revise. Publish first with publish().');
|
|
186
|
+
}
|
|
187
|
+
const { token } = await this.confirmationTokenService.createRequest(this.userId, 'revise_memory', {
|
|
188
|
+
memory_id: input.memory_id,
|
|
189
|
+
space_ids: spaceIds,
|
|
190
|
+
group_ids: groupIds,
|
|
191
|
+
});
|
|
192
|
+
this.logger.info('Revise confirmation created', {
|
|
193
|
+
userId: this.userId,
|
|
194
|
+
memoryId: input.memory_id,
|
|
195
|
+
spaceIds,
|
|
196
|
+
groupIds,
|
|
197
|
+
});
|
|
198
|
+
return { token };
|
|
199
|
+
}
|
|
200
|
+
// ── Confirm (phase 2: execute pending action) ───────────────────────
|
|
201
|
+
async confirm(input) {
|
|
202
|
+
const request = await this.confirmationTokenService.confirmRequest(this.userId, input.token);
|
|
203
|
+
if (!request) {
|
|
204
|
+
throw new Error('Invalid or expired confirmation token');
|
|
205
|
+
}
|
|
206
|
+
if (request.action === 'publish_memory') {
|
|
207
|
+
return this.executePublish(request);
|
|
208
|
+
}
|
|
209
|
+
if (request.action === 'retract_memory') {
|
|
210
|
+
return this.executeRetract(request);
|
|
211
|
+
}
|
|
212
|
+
if (request.action === 'revise_memory') {
|
|
213
|
+
return this.executeRevise(request);
|
|
214
|
+
}
|
|
215
|
+
throw new Error(`Unknown action type: ${request.action}`);
|
|
216
|
+
}
|
|
217
|
+
// ── Deny ────────────────────────────────────────────────────────────
|
|
218
|
+
async deny(input) {
|
|
219
|
+
const success = await this.confirmationTokenService.denyRequest(this.userId, input.token);
|
|
220
|
+
if (!success) {
|
|
221
|
+
throw new Error('Token not found or already used');
|
|
222
|
+
}
|
|
223
|
+
return { success: true };
|
|
224
|
+
}
|
|
225
|
+
// ── Moderate ────────────────────────────────────────────────────────
|
|
226
|
+
async moderate(input, authContext) {
|
|
227
|
+
if (!input.space_id && !input.group_id) {
|
|
228
|
+
throw new Error('Must specify either space_id or group_id');
|
|
229
|
+
}
|
|
230
|
+
if (!ACTION_TO_STATUS[input.action]) {
|
|
231
|
+
throw new Error(`Invalid action: ${input.action}. Must be approve, reject, or remove`);
|
|
232
|
+
}
|
|
233
|
+
// Permission check
|
|
234
|
+
if (input.group_id) {
|
|
235
|
+
if (!canModerate(authContext, input.group_id)) {
|
|
236
|
+
throw new Error(`Moderator access required for group ${input.group_id}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else if (input.space_id) {
|
|
240
|
+
if (!canModerateAny(authContext)) {
|
|
241
|
+
throw new Error('Moderator access required to moderate memories in spaces');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Get the collection
|
|
245
|
+
let collection;
|
|
246
|
+
if (input.group_id) {
|
|
247
|
+
const collectionName = getCollectionName(CollectionType.GROUPS, input.group_id);
|
|
248
|
+
collection = this.weaviateClient.collections.get(collectionName);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
collection = await ensurePublicCollection(this.weaviateClient);
|
|
252
|
+
}
|
|
253
|
+
// Fetch the memory
|
|
254
|
+
const memory = await fetchMemoryWithAllProperties(collection, input.memory_id);
|
|
255
|
+
if (!memory) {
|
|
256
|
+
const location = input.group_id ? `group ${input.group_id}` : `space ${input.space_id}`;
|
|
257
|
+
throw new Error(`Published memory ${input.memory_id} not found in ${location}`);
|
|
258
|
+
}
|
|
259
|
+
// Update moderation fields
|
|
260
|
+
const newStatus = ACTION_TO_STATUS[input.action];
|
|
261
|
+
const now = new Date().toISOString();
|
|
262
|
+
await collection.data.update({
|
|
263
|
+
id: input.memory_id,
|
|
264
|
+
properties: {
|
|
265
|
+
moderation_status: newStatus,
|
|
266
|
+
moderated_by: this.userId,
|
|
267
|
+
moderated_at: now,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
const location = input.group_id ? `group:${input.group_id}` : `space:${input.space_id}`;
|
|
271
|
+
this.logger.info('Memory moderated', {
|
|
272
|
+
userId: this.userId,
|
|
273
|
+
memoryId: input.memory_id,
|
|
274
|
+
action: input.action,
|
|
275
|
+
newStatus,
|
|
276
|
+
location,
|
|
277
|
+
});
|
|
278
|
+
return {
|
|
279
|
+
memory_id: input.memory_id,
|
|
280
|
+
action: input.action,
|
|
281
|
+
moderation_status: newStatus,
|
|
282
|
+
moderated_by: this.userId,
|
|
283
|
+
moderated_at: now,
|
|
284
|
+
location,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
// ── Search Space ────────────────────────────────────────────────────
|
|
288
|
+
async search(input, authContext) {
|
|
289
|
+
const spaces = input.spaces || [];
|
|
290
|
+
const groups = input.groups || [];
|
|
291
|
+
const searchType = input.search_type || 'hybrid';
|
|
292
|
+
const limit = input.limit ?? 10;
|
|
293
|
+
const offset = input.offset ?? 0;
|
|
294
|
+
// Validate space IDs
|
|
295
|
+
if (spaces.length > 0) {
|
|
296
|
+
const invalidSpaces = spaces.filter((s) => !isValidSpaceId(s));
|
|
297
|
+
if (invalidSpaces.length > 0) {
|
|
298
|
+
throw new Error(`Invalid space IDs: ${invalidSpaces.join(', ')}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Validate group IDs
|
|
302
|
+
if (groups.length > 0) {
|
|
303
|
+
const invalidGroups = groups.filter((g) => !g || g.includes('.') || g.trim() === '');
|
|
304
|
+
if (invalidGroups.length > 0) {
|
|
305
|
+
throw new Error('Group IDs cannot be empty or contain dots');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Permission check for non-approved moderation filters
|
|
309
|
+
const moderationFilter = input.moderation_filter || 'approved';
|
|
310
|
+
if (moderationFilter !== 'approved') {
|
|
311
|
+
for (const groupId of groups) {
|
|
312
|
+
if (!canModerate(authContext, groupId)) {
|
|
313
|
+
throw new Error(`Moderator access required to view ${moderationFilter} memories in group ${groupId}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if ((spaces.length > 0 || groups.length === 0) && !canModerateAny(authContext)) {
|
|
317
|
+
throw new Error(`Moderator access required to view ${moderationFilter} memories in spaces`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const fetchLimit = (limit + offset) * Math.max(1, groups.length + (spaces.length > 0 || groups.length === 0 ? 1 : 0));
|
|
321
|
+
const allObjects = [];
|
|
322
|
+
// Search spaces collection (when spaces specified or neither spaces nor groups)
|
|
323
|
+
if (spaces.length > 0 || groups.length === 0) {
|
|
324
|
+
const spacesCollectionName = getCollectionName(CollectionType.SPACES);
|
|
325
|
+
const spacesCollection = this.weaviateClient.collections.get(spacesCollectionName);
|
|
326
|
+
const filterList = this.buildBaseFilters(spacesCollection, input);
|
|
327
|
+
if (spaces.length > 0) {
|
|
328
|
+
filterList.push(spacesCollection.filter.byProperty('space_ids').containsAny(spaces));
|
|
329
|
+
}
|
|
330
|
+
const combinedFilters = filterList.length > 0 ? Filters.and(...filterList) : undefined;
|
|
331
|
+
const spaceObjects = await this.executeSearch(spacesCollection, input.query, searchType, combinedFilters, fetchLimit);
|
|
332
|
+
allObjects.push(...spaceObjects);
|
|
333
|
+
}
|
|
334
|
+
// Search group collections
|
|
335
|
+
for (const groupId of groups) {
|
|
336
|
+
const groupCollectionName = getCollectionName(CollectionType.GROUPS, groupId);
|
|
337
|
+
const exists = await this.weaviateClient.collections.exists(groupCollectionName);
|
|
338
|
+
if (!exists)
|
|
339
|
+
continue;
|
|
340
|
+
const groupCollection = this.weaviateClient.collections.get(groupCollectionName);
|
|
341
|
+
const filterList = this.buildBaseFilters(groupCollection, input);
|
|
342
|
+
const combinedFilters = filterList.length > 0 ? Filters.and(...filterList) : undefined;
|
|
343
|
+
const groupObjects = await this.executeSearch(groupCollection, input.query, searchType, combinedFilters, fetchLimit);
|
|
344
|
+
allObjects.push(...groupObjects);
|
|
345
|
+
}
|
|
346
|
+
// Deduplicate by UUID
|
|
347
|
+
const seen = new Set();
|
|
348
|
+
const deduplicated = allObjects.filter((obj) => {
|
|
349
|
+
if (seen.has(obj.uuid))
|
|
350
|
+
return false;
|
|
351
|
+
seen.add(obj.uuid);
|
|
352
|
+
return true;
|
|
353
|
+
});
|
|
354
|
+
// Sort by relevance score
|
|
355
|
+
deduplicated.sort((a, b) => {
|
|
356
|
+
const scoreA = a.metadata?.score ?? 0;
|
|
357
|
+
const scoreB = b.metadata?.score ?? 0;
|
|
358
|
+
return scoreB - scoreA;
|
|
359
|
+
});
|
|
360
|
+
// Paginate
|
|
361
|
+
const paginated = deduplicated.slice(offset, offset + limit);
|
|
362
|
+
const memories = paginated.map((obj) => ({
|
|
363
|
+
id: obj.uuid,
|
|
364
|
+
...obj.properties,
|
|
365
|
+
}));
|
|
366
|
+
const isAllPublic = spaces.length === 0 && groups.length === 0;
|
|
367
|
+
return {
|
|
368
|
+
spaces_searched: isAllPublic ? 'all_public' : spaces,
|
|
369
|
+
groups_searched: groups,
|
|
370
|
+
memories,
|
|
371
|
+
total: memories.length,
|
|
372
|
+
offset,
|
|
373
|
+
limit,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
// ── Query Space ─────────────────────────────────────────────────────
|
|
377
|
+
async query(input, authContext) {
|
|
378
|
+
if (!input.question?.trim())
|
|
379
|
+
throw new Error('Question cannot be empty');
|
|
380
|
+
// Validate space IDs
|
|
381
|
+
if (input.spaces.length === 0)
|
|
382
|
+
throw new Error('Must specify at least one space to query');
|
|
383
|
+
const invalidSpaces = input.spaces.filter((s) => !isValidSpaceId(s));
|
|
384
|
+
if (invalidSpaces.length > 0) {
|
|
385
|
+
throw new Error(`Invalid space IDs: ${invalidSpaces.join(', ')}`);
|
|
386
|
+
}
|
|
387
|
+
// Permission check for non-approved moderation filters
|
|
388
|
+
const moderationFilterValue = input.moderation_filter || 'approved';
|
|
389
|
+
if (moderationFilterValue !== 'approved' && !canModerateAny(authContext)) {
|
|
390
|
+
throw new Error(`Moderator access required to view ${moderationFilterValue} memories in spaces`);
|
|
391
|
+
}
|
|
392
|
+
const publicCollection = await ensurePublicCollection(this.weaviateClient);
|
|
393
|
+
const filterList = [];
|
|
394
|
+
// Filter by spaces
|
|
395
|
+
filterList.push(publicCollection.filter.byProperty('spaces').containsAny(input.spaces));
|
|
396
|
+
// Filter by doc_type
|
|
397
|
+
filterList.push(publicCollection.filter.byProperty('doc_type').equal('memory'));
|
|
398
|
+
// Moderation filter
|
|
399
|
+
const moderationFilter = buildModerationFilter(publicCollection, input.moderation_filter);
|
|
400
|
+
if (moderationFilter)
|
|
401
|
+
filterList.push(moderationFilter);
|
|
402
|
+
// Content type filter
|
|
403
|
+
if (input.content_type) {
|
|
404
|
+
filterList.push(publicCollection.filter.byProperty('content_type').equal(input.content_type));
|
|
405
|
+
}
|
|
406
|
+
// Exclude comments by default
|
|
407
|
+
if (!input.include_comments && !input.content_type) {
|
|
408
|
+
filterList.push(publicCollection.filter.byProperty('content_type').notEqual('comment'));
|
|
409
|
+
}
|
|
410
|
+
// Tags filter (AND semantics)
|
|
411
|
+
if (input.tags?.length) {
|
|
412
|
+
input.tags.forEach((tag) => {
|
|
413
|
+
filterList.push(publicCollection.filter.byProperty('tags').containsAny([tag]));
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
// Weight filter
|
|
417
|
+
if (input.min_weight !== undefined) {
|
|
418
|
+
filterList.push(publicCollection.filter.byProperty('weight').greaterOrEqual(input.min_weight));
|
|
419
|
+
}
|
|
420
|
+
// Date filters
|
|
421
|
+
if (input.date_from) {
|
|
422
|
+
filterList.push(publicCollection.filter.byProperty('created_at').greaterOrEqual(new Date(input.date_from)));
|
|
423
|
+
}
|
|
424
|
+
if (input.date_to) {
|
|
425
|
+
filterList.push(publicCollection.filter.byProperty('created_at').lessOrEqual(new Date(input.date_to)));
|
|
426
|
+
}
|
|
427
|
+
const combinedFilters = filterList.length > 0 ? Filters.and(...filterList) : undefined;
|
|
428
|
+
const opts = { limit: input.limit ?? 10 };
|
|
429
|
+
if (combinedFilters)
|
|
430
|
+
opts.filters = combinedFilters;
|
|
431
|
+
const results = await publicCollection.query.nearText(input.question, opts);
|
|
432
|
+
const memories = results.objects.map((obj) => ({
|
|
433
|
+
id: obj.uuid,
|
|
434
|
+
...obj.properties,
|
|
435
|
+
_distance: obj.metadata?.distance,
|
|
436
|
+
}));
|
|
437
|
+
return {
|
|
438
|
+
question: input.question,
|
|
439
|
+
spaces_queried: input.spaces,
|
|
440
|
+
memories,
|
|
441
|
+
total: memories.length,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
// ── Private: Execute Publish ────────────────────────────────────────
|
|
445
|
+
async executePublish(request) {
|
|
446
|
+
const spaces = request.payload.spaces || [];
|
|
447
|
+
const groups = request.payload.groups || [];
|
|
448
|
+
if (spaces.length === 0 && groups.length === 0) {
|
|
449
|
+
throw new Error('No destinations in publish request');
|
|
450
|
+
}
|
|
451
|
+
// Fetch the memory fresh
|
|
452
|
+
const originalMemory = await fetchMemoryWithAllProperties(this.userCollection, request.payload.memory_id);
|
|
453
|
+
if (!originalMemory)
|
|
454
|
+
throw new Error(`Memory ${request.payload.memory_id} no longer exists`);
|
|
455
|
+
if (originalMemory.properties.user_id !== this.userId)
|
|
456
|
+
throw new Error('Permission denied');
|
|
457
|
+
const compositeId = generateCompositeId(this.userId, request.payload.memory_id);
|
|
458
|
+
// Existing tracking arrays
|
|
459
|
+
const existingSpaceIds = Array.isArray(originalMemory.properties.space_ids)
|
|
460
|
+
? originalMemory.properties.space_ids
|
|
461
|
+
: [];
|
|
462
|
+
const existingGroupIds = Array.isArray(originalMemory.properties.group_ids)
|
|
463
|
+
? originalMemory.properties.group_ids
|
|
464
|
+
: [];
|
|
465
|
+
// Merge tags
|
|
466
|
+
const originalTags = Array.isArray(originalMemory.properties.tags)
|
|
467
|
+
? originalMemory.properties.tags
|
|
468
|
+
: [];
|
|
469
|
+
const additionalTags = Array.isArray(request.payload.additional_tags)
|
|
470
|
+
? request.payload.additional_tags
|
|
471
|
+
: [];
|
|
472
|
+
const mergedTags = [...originalTags, ...additionalTags];
|
|
473
|
+
const successfulPublications = [];
|
|
474
|
+
const failedPublications = [];
|
|
475
|
+
// Publish to spaces (Memory_spaces_public)
|
|
476
|
+
if (spaces.length > 0) {
|
|
477
|
+
try {
|
|
478
|
+
const publicCollection = await ensurePublicCollection(this.weaviateClient);
|
|
479
|
+
let existingSpaceMemory = null;
|
|
480
|
+
try {
|
|
481
|
+
existingSpaceMemory = await fetchMemoryWithAllProperties(publicCollection, compositeId);
|
|
482
|
+
}
|
|
483
|
+
catch { /* doesn't exist */ }
|
|
484
|
+
const newSpaceIds = [...new Set([...existingSpaceIds, ...spaces])];
|
|
485
|
+
// Determine moderation status
|
|
486
|
+
let spaceModerationStatus = 'approved';
|
|
487
|
+
for (const spaceId of spaces) {
|
|
488
|
+
const spaceConfig = await getSpaceConfig(spaceId, 'space');
|
|
489
|
+
if (spaceConfig.require_moderation) {
|
|
490
|
+
spaceModerationStatus = 'pending';
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const publishedMemory = {
|
|
495
|
+
...originalMemory.properties,
|
|
496
|
+
id: compositeId,
|
|
497
|
+
space_ids: newSpaceIds,
|
|
498
|
+
group_ids: existingGroupIds,
|
|
499
|
+
spaces,
|
|
500
|
+
author_id: this.userId,
|
|
501
|
+
published_at: new Date().toISOString(),
|
|
502
|
+
discovery_count: 0,
|
|
503
|
+
attribution: 'user',
|
|
504
|
+
moderation_status: spaceModerationStatus,
|
|
505
|
+
tags: mergedTags,
|
|
506
|
+
};
|
|
507
|
+
delete publishedMemory._additional;
|
|
508
|
+
if (existingSpaceMemory) {
|
|
509
|
+
await publicCollection.data.update({ id: compositeId, properties: publishedMemory });
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
await publicCollection.data.insert({ id: compositeId, properties: publishedMemory });
|
|
513
|
+
}
|
|
514
|
+
successfulPublications.push(`spaces: ${spaces.join(', ')}`);
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
failedPublications.push(`spaces: ${err instanceof Error ? err.message : String(err)}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// Publish to groups
|
|
521
|
+
for (const groupId of groups) {
|
|
522
|
+
const groupCollectionName = getCollectionName(CollectionType.GROUPS, groupId);
|
|
523
|
+
try {
|
|
524
|
+
const groupCollection = this.weaviateClient.collections.get(groupCollectionName);
|
|
525
|
+
let existingGroupMemory = null;
|
|
526
|
+
try {
|
|
527
|
+
existingGroupMemory = await fetchMemoryWithAllProperties(groupCollection, compositeId);
|
|
528
|
+
}
|
|
529
|
+
catch { /* doesn't exist */ }
|
|
530
|
+
const newGroupIds = [...new Set([...existingGroupIds, groupId])];
|
|
531
|
+
const groupConfig = await getSpaceConfig(groupId, 'group');
|
|
532
|
+
const groupModerationStatus = groupConfig.require_moderation ? 'pending' : 'approved';
|
|
533
|
+
const groupMemory = {
|
|
534
|
+
...originalMemory.properties,
|
|
535
|
+
id: compositeId,
|
|
536
|
+
space_ids: existingSpaceIds,
|
|
537
|
+
group_ids: newGroupIds,
|
|
538
|
+
author_id: this.userId,
|
|
539
|
+
published_at: new Date().toISOString(),
|
|
540
|
+
discovery_count: 0,
|
|
541
|
+
attribution: 'user',
|
|
542
|
+
moderation_status: groupModerationStatus,
|
|
543
|
+
tags: mergedTags,
|
|
544
|
+
};
|
|
545
|
+
delete groupMemory._additional;
|
|
546
|
+
if (existingGroupMemory) {
|
|
547
|
+
await groupCollection.data.update({ id: compositeId, properties: groupMemory });
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
await groupCollection.data.insert({ id: compositeId, properties: groupMemory });
|
|
551
|
+
}
|
|
552
|
+
successfulPublications.push(`group: ${groupId}`);
|
|
553
|
+
}
|
|
554
|
+
catch (err) {
|
|
555
|
+
failedPublications.push(`group ${groupId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// Update source memory tracking arrays
|
|
559
|
+
const finalSpaceIds = successfulPublications.some((p) => p.startsWith('spaces:'))
|
|
560
|
+
? [...new Set([...existingSpaceIds, ...spaces])]
|
|
561
|
+
: existingSpaceIds;
|
|
562
|
+
const successfulGroups = groups.filter((g) => successfulPublications.some((p) => p === `group: ${g}`));
|
|
563
|
+
const finalGroupIds = [...new Set([...existingGroupIds, ...successfulGroups])];
|
|
564
|
+
if (finalSpaceIds.length > existingSpaceIds.length || finalGroupIds.length > existingGroupIds.length) {
|
|
565
|
+
try {
|
|
566
|
+
await this.userCollection.data.update({
|
|
567
|
+
id: request.payload.memory_id,
|
|
568
|
+
properties: { space_ids: finalSpaceIds, group_ids: finalGroupIds },
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
this.logger.warn('Failed to update source memory tracking arrays');
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (successfulPublications.length === 0) {
|
|
576
|
+
throw new Error(`Publication failed: ${failedPublications.join('; ')}`);
|
|
577
|
+
}
|
|
578
|
+
this.logger.info('Memory published', {
|
|
579
|
+
compositeId,
|
|
580
|
+
published: successfulPublications,
|
|
581
|
+
failed: failedPublications,
|
|
582
|
+
});
|
|
583
|
+
return {
|
|
584
|
+
action: 'publish_memory',
|
|
585
|
+
success: true,
|
|
586
|
+
composite_id: compositeId,
|
|
587
|
+
published_to: successfulPublications,
|
|
588
|
+
failed: failedPublications.length > 0 ? failedPublications : undefined,
|
|
589
|
+
space_ids: finalSpaceIds,
|
|
590
|
+
group_ids: finalGroupIds,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
// ── Private: Execute Retract ────────────────────────────────────────
|
|
594
|
+
async executeRetract(request) {
|
|
595
|
+
const spaces = request.payload.spaces || [];
|
|
596
|
+
const groups = request.payload.groups || [];
|
|
597
|
+
const sourceMemory = await fetchMemoryWithAllProperties(this.userCollection, request.payload.memory_id);
|
|
598
|
+
if (!sourceMemory)
|
|
599
|
+
throw new Error(`Source memory ${request.payload.memory_id} no longer exists`);
|
|
600
|
+
const currentSpaceIds = Array.isArray(sourceMemory.properties.space_ids)
|
|
601
|
+
? sourceMemory.properties.space_ids
|
|
602
|
+
: [];
|
|
603
|
+
const currentGroupIds = Array.isArray(sourceMemory.properties.group_ids)
|
|
604
|
+
? sourceMemory.properties.group_ids
|
|
605
|
+
: [];
|
|
606
|
+
const compositeId = generateCompositeId(this.userId, request.payload.memory_id);
|
|
607
|
+
const successfulRetractions = [];
|
|
608
|
+
const failedRetractions = [];
|
|
609
|
+
// Retract from spaces (orphan: remove from space_ids but keep the document)
|
|
610
|
+
if (spaces.length > 0) {
|
|
611
|
+
try {
|
|
612
|
+
const spacesCollectionName = getCollectionName(CollectionType.SPACES);
|
|
613
|
+
const publicCollection = this.weaviateClient.collections.get(spacesCollectionName);
|
|
614
|
+
const publishedMemory = await fetchMemoryWithAllProperties(publicCollection, compositeId);
|
|
615
|
+
if (publishedMemory) {
|
|
616
|
+
const newSpaceIds = currentSpaceIds.filter((id) => !spaces.includes(id));
|
|
617
|
+
await publicCollection.data.update({
|
|
618
|
+
id: compositeId,
|
|
619
|
+
properties: { space_ids: newSpaceIds, retracted_at: new Date().toISOString() },
|
|
620
|
+
});
|
|
621
|
+
successfulRetractions.push(`spaces: ${spaces.join(', ')}`);
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
failedRetractions.push('spaces: Memory not found in spaces collection');
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch (err) {
|
|
628
|
+
failedRetractions.push(`spaces: ${err instanceof Error ? err.message : String(err)}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// Retract from groups (orphan: remove from group_ids but keep the document)
|
|
632
|
+
for (const groupId of groups) {
|
|
633
|
+
const groupCollectionName = getCollectionName(CollectionType.GROUPS, groupId);
|
|
634
|
+
try {
|
|
635
|
+
const groupCollection = this.weaviateClient.collections.get(groupCollectionName);
|
|
636
|
+
const groupMemory = await fetchMemoryWithAllProperties(groupCollection, compositeId);
|
|
637
|
+
if (groupMemory) {
|
|
638
|
+
const groupMemoryGroupIds = Array.isArray(groupMemory.properties.group_ids)
|
|
639
|
+
? groupMemory.properties.group_ids
|
|
640
|
+
: [];
|
|
641
|
+
const newGroupIds = groupMemoryGroupIds.filter((id) => id !== groupId);
|
|
642
|
+
await groupCollection.data.update({
|
|
643
|
+
id: compositeId,
|
|
644
|
+
properties: { group_ids: newGroupIds, retracted_at: new Date().toISOString() },
|
|
645
|
+
});
|
|
646
|
+
successfulRetractions.push(`group: ${groupId}`);
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
failedRetractions.push(`group ${groupId}: Memory not found in group`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
catch (err) {
|
|
653
|
+
failedRetractions.push(`group ${groupId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
// Update source memory tracking arrays
|
|
657
|
+
const finalSpaceIds = successfulRetractions.some((r) => r.startsWith('spaces:'))
|
|
658
|
+
? currentSpaceIds.filter((id) => !spaces.includes(id))
|
|
659
|
+
: currentSpaceIds;
|
|
660
|
+
const successfulGroupRetractions = groups.filter((g) => successfulRetractions.some((r) => r === `group: ${g}`));
|
|
661
|
+
const finalGroupIds = currentGroupIds.filter((id) => !successfulGroupRetractions.includes(id));
|
|
662
|
+
try {
|
|
663
|
+
await this.userCollection.data.update({
|
|
664
|
+
id: request.payload.memory_id,
|
|
665
|
+
properties: { space_ids: finalSpaceIds, group_ids: finalGroupIds },
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
this.logger.warn('Failed to update source memory tracking arrays after retraction');
|
|
670
|
+
}
|
|
671
|
+
if (successfulRetractions.length === 0) {
|
|
672
|
+
throw new Error(`Retraction failed: ${failedRetractions.join('; ')}`);
|
|
673
|
+
}
|
|
674
|
+
this.logger.info('Memory retracted', {
|
|
675
|
+
compositeId,
|
|
676
|
+
retracted: successfulRetractions,
|
|
677
|
+
failed: failedRetractions,
|
|
678
|
+
});
|
|
679
|
+
return {
|
|
680
|
+
action: 'retract_memory',
|
|
681
|
+
success: true,
|
|
682
|
+
composite_id: compositeId,
|
|
683
|
+
retracted_from: successfulRetractions,
|
|
684
|
+
failed: failedRetractions.length > 0 ? failedRetractions : undefined,
|
|
685
|
+
space_ids: finalSpaceIds,
|
|
686
|
+
group_ids: finalGroupIds,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
// ── Private: Execute Revise ─────────────────────────────────────────
|
|
690
|
+
async executeRevise(request) {
|
|
691
|
+
const { memory_id, space_ids = [], group_ids = [] } = request.payload;
|
|
692
|
+
const sourceMemory = await fetchMemoryWithAllProperties(this.userCollection, memory_id);
|
|
693
|
+
if (!sourceMemory)
|
|
694
|
+
throw new Error(`Source memory ${memory_id} no longer exists`);
|
|
695
|
+
if (sourceMemory.properties.user_id !== this.userId)
|
|
696
|
+
throw new Error('Permission denied');
|
|
697
|
+
const newContent = String(sourceMemory.properties.content ?? '');
|
|
698
|
+
const revisedAt = new Date().toISOString();
|
|
699
|
+
const compositeId = generateCompositeId(this.userId, memory_id);
|
|
700
|
+
const results = [];
|
|
701
|
+
const reviseInCollection = async (collectionName, locationLabel) => {
|
|
702
|
+
try {
|
|
703
|
+
const collection = this.weaviateClient.collections.get(collectionName);
|
|
704
|
+
const publishedMemory = await fetchMemoryWithAllProperties(collection, compositeId);
|
|
705
|
+
if (!publishedMemory) {
|
|
706
|
+
results.push({ location: locationLabel, status: 'skipped', error: 'Published copy not found' });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const oldContent = String(publishedMemory.properties.content ?? '');
|
|
710
|
+
let revisionHistory = parseRevisionHistory(publishedMemory.properties.revision_history);
|
|
711
|
+
if (oldContent !== newContent) {
|
|
712
|
+
revisionHistory = buildRevisionHistory(revisionHistory, oldContent, revisedAt);
|
|
713
|
+
}
|
|
714
|
+
const currentRevisionCount = typeof publishedMemory.properties.revision_count === 'number'
|
|
715
|
+
? publishedMemory.properties.revision_count
|
|
716
|
+
: 0;
|
|
717
|
+
await collection.data.update({
|
|
718
|
+
id: compositeId,
|
|
719
|
+
properties: {
|
|
720
|
+
content: newContent,
|
|
721
|
+
revised_at: revisedAt,
|
|
722
|
+
revision_count: currentRevisionCount + 1,
|
|
723
|
+
revision_history: JSON.stringify(revisionHistory),
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
results.push({ location: locationLabel, status: 'success' });
|
|
727
|
+
}
|
|
728
|
+
catch (err) {
|
|
729
|
+
results.push({
|
|
730
|
+
location: locationLabel,
|
|
731
|
+
status: 'failed',
|
|
732
|
+
error: err instanceof Error ? err.message : String(err),
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
// Revise in spaces collection
|
|
737
|
+
if (space_ids.length > 0) {
|
|
738
|
+
await reviseInCollection(getCollectionName(CollectionType.SPACES), 'Memory_spaces_public');
|
|
739
|
+
}
|
|
740
|
+
// Revise in each group collection
|
|
741
|
+
for (const groupId of group_ids) {
|
|
742
|
+
await reviseInCollection(getCollectionName(CollectionType.GROUPS, groupId), `Memory_groups_${groupId}`);
|
|
743
|
+
}
|
|
744
|
+
const successCount = results.filter((r) => r.status === 'success').length;
|
|
745
|
+
this.logger.info('Memory revised', {
|
|
746
|
+
compositeId,
|
|
747
|
+
success: successCount,
|
|
748
|
+
total: results.length,
|
|
749
|
+
});
|
|
750
|
+
return {
|
|
751
|
+
action: 'revise_memory',
|
|
752
|
+
success: successCount > 0,
|
|
753
|
+
composite_id: compositeId,
|
|
754
|
+
revised_at: revisedAt,
|
|
755
|
+
results,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
// ── Private: Build Base Filters ─────────────────────────────────────
|
|
759
|
+
buildBaseFilters(collection, input) {
|
|
760
|
+
const filterList = [];
|
|
761
|
+
// Exclude soft-deleted
|
|
762
|
+
filterList.push(collection.filter.byProperty('deleted_at').isNull(true));
|
|
763
|
+
// Only memories
|
|
764
|
+
filterList.push(collection.filter.byProperty('doc_type').equal('memory'));
|
|
765
|
+
// Moderation filter
|
|
766
|
+
const moderationFilter = buildModerationFilter(collection, input.moderation_filter);
|
|
767
|
+
if (moderationFilter)
|
|
768
|
+
filterList.push(moderationFilter);
|
|
769
|
+
// Content type
|
|
770
|
+
if (input.content_type) {
|
|
771
|
+
filterList.push(collection.filter.byProperty('content_type').equal(input.content_type));
|
|
772
|
+
}
|
|
773
|
+
// Exclude comments by default
|
|
774
|
+
if (!input.include_comments && !input.content_type) {
|
|
775
|
+
filterList.push(collection.filter.byProperty('content_type').notEqual('comment'));
|
|
776
|
+
}
|
|
777
|
+
// Tags (AND semantics)
|
|
778
|
+
if (input.tags?.length) {
|
|
779
|
+
input.tags.forEach((tag) => {
|
|
780
|
+
filterList.push(collection.filter.byProperty('tags').containsAny([tag]));
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
// Weight filters
|
|
784
|
+
if (input.min_weight !== undefined) {
|
|
785
|
+
filterList.push(collection.filter.byProperty('weight').greaterOrEqual(input.min_weight));
|
|
786
|
+
}
|
|
787
|
+
if (input.max_weight !== undefined) {
|
|
788
|
+
filterList.push(collection.filter.byProperty('weight').lessOrEqual(input.max_weight));
|
|
789
|
+
}
|
|
790
|
+
// Date filters
|
|
791
|
+
if (input.date_from) {
|
|
792
|
+
filterList.push(collection.filter.byProperty('created_at').greaterOrEqual(new Date(input.date_from)));
|
|
793
|
+
}
|
|
794
|
+
if (input.date_to) {
|
|
795
|
+
filterList.push(collection.filter.byProperty('created_at').lessOrEqual(new Date(input.date_to)));
|
|
796
|
+
}
|
|
797
|
+
return filterList;
|
|
798
|
+
}
|
|
799
|
+
// ── Private: Execute Search ─────────────────────────────────────────
|
|
800
|
+
async executeSearch(collection, query, searchType, filters, limit) {
|
|
801
|
+
const opts = { limit };
|
|
802
|
+
if (filters)
|
|
803
|
+
opts.filters = filters;
|
|
804
|
+
switch (searchType) {
|
|
805
|
+
case 'bm25':
|
|
806
|
+
return (await collection.query.bm25(query, opts)).objects;
|
|
807
|
+
case 'semantic':
|
|
808
|
+
return (await collection.query.nearText([query], opts)).objects;
|
|
809
|
+
case 'hybrid':
|
|
810
|
+
default:
|
|
811
|
+
return (await collection.query.hybrid(query, opts)).objects;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
//# sourceMappingURL=space.service.js.map
|