@nlabs/metropolisjs 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +167 -0
- package/AGENTS.md +13 -0
- package/CHANGELOG.md +112 -0
- package/GRAPHQL_VALIDATION_RESULTS.md +158 -0
- package/PERMISSION_GRAPHQL_INTEGRATION.md +293 -0
- package/PERMISSION_IMPLEMENTATION_SUMMARY.md +243 -0
- package/README.md +1123 -112
- package/SIGNIN_GRAPHQL_VALIDATION.md +125 -0
- package/VAULT_GRAPHQL_API_FINDINGS.md +312 -0
- package/docs/ACTIONS.md +63 -0
- package/docs/COLLECTIONS.md +1038 -0
- package/docs/CONNECTIONS.md +1367 -0
- package/docs/CRUD_INTEGRATION.md +702 -0
- package/examples/connections-usage.tsx +909 -0
- package/examples/content-actions-usage.ts +144 -0
- package/examples/crud-usage.tsx +847 -0
- package/examples/extensibility-usage.tsx +889 -0
- package/examples/i18n-import-examples.ts +86 -0
- package/examples/i18n-namespace-usage.ts +118 -0
- package/examples/i18next-singleton-demo.ts +187 -0
- package/examples/permission-system-usage.tsx +415 -0
- package/examples/project-details.tsx +397 -0
- package/examples/shared-i18n-usage.tsx +203 -0
- package/examples/signup-error-handling.ts +100 -0
- package/examples/translation-types.ts +86 -0
- package/examples/translation-usage.tsx +336 -0
- package/index.js +1 -0
- package/jest.config.mjs +28 -0
- package/lex.config.mjs +11 -2
- package/lib/__tests__/e2e/helpers/testGraphQLServer.d.ts +22 -0
- package/lib/__tests__/e2e/helpers/testGraphQLServer.d.ts.map +1 -0
- package/lib/__tests__/e2e/helpers/testGraphQLServer.js +197 -0
- package/lib/__tests__/e2e/helpers/testGraphQLServerSimple.d.ts +3 -0
- package/lib/__tests__/e2e/helpers/testGraphQLServerSimple.d.ts.map +1 -0
- package/lib/__tests__/e2e/helpers/testGraphQLServerSimple.js +52 -0
- package/lib/actions/appActions/appActions.d.ts +29 -0
- package/lib/actions/appActions/appActions.d.ts.map +1 -0
- package/lib/actions/appActions/appActions.js +159 -0
- package/lib/actions/connectionActions/connectionActions.d.ts +19 -0
- package/lib/actions/connectionActions/connectionActions.d.ts.map +1 -0
- package/lib/actions/connectionActions/connectionActions.js +131 -0
- package/lib/actions/contentActions/contentActions.d.ts +34 -0
- package/lib/actions/contentActions/contentActions.d.ts.map +1 -0
- package/lib/actions/contentActions/contentActions.js +314 -0
- package/lib/actions/conversationActions/conversationActions.d.ts +30 -0
- package/lib/actions/conversationActions/conversationActions.d.ts.map +1 -0
- package/lib/actions/conversationActions/conversationActions.js +225 -0
- package/lib/actions/eventActions/eventActions.d.ts +27 -7
- package/lib/actions/eventActions/eventActions.d.ts.map +1 -0
- package/lib/actions/eventActions/eventActions.js +436 -2
- package/lib/actions/groupActions/groupActions.d.ts +30 -0
- package/lib/actions/groupActions/groupActions.d.ts.map +1 -0
- package/lib/actions/groupActions/groupActions.js +229 -0
- package/lib/actions/imageActions/imageActions.d.ts +16 -10
- package/lib/actions/imageActions/imageActions.d.ts.map +1 -0
- package/lib/actions/imageActions/imageActions.js +593 -2
- package/lib/actions/index.d.ts +21 -9
- package/lib/actions/index.d.ts.map +1 -0
- package/lib/actions/index.js +22 -2
- package/lib/actions/locationActions/locationActions.d.ts +11 -9
- package/lib/actions/locationActions/locationActions.d.ts.map +1 -0
- package/lib/actions/locationActions/locationActions.js +388 -2
- package/lib/actions/messageActions/messageActions.d.ts +16 -32
- package/lib/actions/messageActions/messageActions.d.ts.map +1 -0
- package/lib/actions/messageActions/messageActions.js +235 -2
- package/lib/actions/permissionActions/permissionActions.d.ts +34 -0
- package/lib/actions/permissionActions/permissionActions.d.ts.map +1 -0
- package/lib/actions/permissionActions/permissionActions.js +330 -0
- package/lib/actions/personaActions/personaActions.d.ts +65 -0
- package/lib/actions/personaActions/personaActions.d.ts.map +1 -0
- package/lib/actions/personaActions/personaActions.js +572 -0
- package/lib/actions/postActions/postActions.d.ts +12 -32
- package/lib/actions/postActions/postActions.d.ts.map +1 -0
- package/lib/actions/postActions/postActions.js +447 -2
- package/lib/actions/reactionActions/reactionActions.d.ts +13 -27
- package/lib/actions/reactionActions/reactionActions.d.ts.map +1 -0
- package/lib/actions/reactionActions/reactionActions.js +413 -2
- package/lib/actions/sseActions/sseActions.d.ts +1 -0
- package/lib/actions/sseActions/sseActions.d.ts.map +1 -0
- package/lib/actions/sseActions/sseActions.js +148 -2
- package/lib/actions/subscriptionActions/subscriptionActions.d.ts +24 -0
- package/lib/actions/subscriptionActions/subscriptionActions.d.ts.map +1 -0
- package/lib/actions/subscriptionActions/subscriptionActions.js +329 -0
- package/lib/actions/tagActions/tagActions.d.ts +13 -31
- package/lib/actions/tagActions/tagActions.d.ts.map +1 -0
- package/lib/actions/tagActions/tagActions.js +347 -2
- package/lib/actions/translationActions/translationActions.d.ts +38 -0
- package/lib/actions/translationActions/translationActions.d.ts.map +1 -0
- package/lib/actions/translationActions/translationActions.js +189 -0
- package/lib/actions/userActions/userActions.d.ts +42 -95
- package/lib/actions/userActions/userActions.d.ts.map +1 -0
- package/lib/actions/userActions/userActions.js +844 -2
- package/lib/actions/videoActions/videoActions.d.ts +41 -0
- package/lib/actions/videoActions/videoActions.d.ts.map +1 -0
- package/lib/actions/videoActions/videoActions.js +532 -0
- package/lib/actions/websocketActions/websocketActions.d.ts +20 -21
- package/lib/actions/websocketActions/websocketActions.d.ts.map +1 -0
- package/lib/actions/websocketActions/websocketActions.js +377 -2
- package/lib/adapters/appAdapter/appAdapter.d.ts +8 -0
- package/lib/adapters/appAdapter/appAdapter.d.ts.map +1 -0
- package/lib/adapters/appAdapter/appAdapter.js +90 -0
- package/lib/adapters/arangoAdapter/arangoAdapter.d.ts +3 -2
- package/lib/adapters/arangoAdapter/arangoAdapter.d.ts.map +1 -0
- package/lib/adapters/arangoAdapter/arangoAdapter.js +75 -2
- package/lib/adapters/contentAdapter/contentAdapter.d.ts +3 -2
- package/lib/adapters/contentAdapter/contentAdapter.d.ts.map +1 -0
- package/lib/adapters/contentAdapter/contentAdapter.js +107 -2
- package/lib/adapters/conversationAdapter/conversationAdapter.d.ts +3 -2
- package/lib/adapters/conversationAdapter/conversationAdapter.d.ts.map +1 -0
- package/lib/adapters/conversationAdapter/conversationAdapter.js +75 -2
- package/lib/adapters/eventAdapter/eventAdapter.d.ts +5 -2
- package/lib/adapters/eventAdapter/eventAdapter.d.ts.map +1 -0
- package/lib/adapters/eventAdapter/eventAdapter.js +162 -2
- package/lib/adapters/fileAdapter/fileAdapter.d.ts +3 -2
- package/lib/adapters/fileAdapter/fileAdapter.d.ts.map +1 -0
- package/lib/adapters/fileAdapter/fileAdapter.js +118 -2
- package/lib/adapters/groupAdapter/groupAdapter.d.ts +28 -0
- package/lib/adapters/groupAdapter/groupAdapter.d.ts.map +1 -0
- package/lib/adapters/groupAdapter/groupAdapter.js +66 -0
- package/lib/adapters/imageAdapter/imageAdapter.d.ts +9 -2
- package/lib/adapters/imageAdapter/imageAdapter.d.ts.map +1 -0
- package/lib/adapters/imageAdapter/imageAdapter.js +134 -2
- package/lib/adapters/index.d.ts +34 -16
- package/lib/adapters/index.d.ts.map +1 -0
- package/lib/adapters/index.js +19 -2
- package/lib/adapters/locationAdapter/locationAdapter.d.ts +3 -2
- package/lib/adapters/locationAdapter/locationAdapter.d.ts.map +1 -0
- package/lib/adapters/locationAdapter/locationAdapter.js +95 -2
- package/lib/adapters/messageAdapter/messageAdapter.d.ts +23 -19
- package/lib/adapters/messageAdapter/messageAdapter.d.ts.map +1 -0
- package/lib/adapters/messageAdapter/messageAdapter.js +82 -2
- package/lib/adapters/permissionAdapter/permissionAdapter.d.ts +41 -0
- package/lib/adapters/permissionAdapter/permissionAdapter.d.ts.map +1 -0
- package/lib/adapters/permissionAdapter/permissionAdapter.js +120 -0
- package/lib/adapters/personaAdapter/personaAdapter.d.ts +28 -2
- package/lib/adapters/personaAdapter/personaAdapter.d.ts.map +1 -0
- package/lib/adapters/personaAdapter/personaAdapter.js +161 -2
- package/lib/adapters/postAdapter/postAdapter.d.ts +3 -2
- package/lib/adapters/postAdapter/postAdapter.d.ts.map +1 -0
- package/lib/adapters/postAdapter/postAdapter.js +115 -2
- package/lib/adapters/reactionAdapter/reactionAdapter.d.ts +5 -2
- package/lib/adapters/reactionAdapter/reactionAdapter.d.ts.map +1 -0
- package/lib/adapters/reactionAdapter/reactionAdapter.js +81 -2
- package/lib/adapters/reaktorAdapter/reaktorAdapter.d.ts +3 -2
- package/lib/adapters/reaktorAdapter/reaktorAdapter.d.ts.map +1 -0
- package/lib/adapters/reaktorAdapter/reaktorAdapter.js +58 -2
- package/lib/adapters/sessionAdapter/sessionAdapter.d.ts +3 -2
- package/lib/adapters/sessionAdapter/sessionAdapter.d.ts.map +1 -0
- package/lib/adapters/sessionAdapter/sessionAdapter.js +80 -2
- package/lib/adapters/tagAdapter/tagAdapter.d.ts +3 -2
- package/lib/adapters/tagAdapter/tagAdapter.d.ts.map +1 -0
- package/lib/adapters/tagAdapter/tagAdapter.js +98 -2
- package/lib/adapters/translationAdapter/translationAdapter.d.ts +21 -0
- package/lib/adapters/translationAdapter/translationAdapter.d.ts.map +1 -0
- package/lib/adapters/translationAdapter/translationAdapter.js +55 -0
- package/lib/adapters/userAdapter/userAdapter.d.ts +18 -6
- package/lib/adapters/userAdapter/userAdapter.d.ts.map +1 -0
- package/lib/adapters/userAdapter/userAdapter.js +302 -2
- package/lib/adapters/videoAdapter/videoAdapter.d.ts +8 -0
- package/lib/adapters/videoAdapter/videoAdapter.d.ts.map +1 -0
- package/lib/adapters/videoAdapter/videoAdapter.js +194 -0
- package/lib/config/index.d.ts +19 -6
- package/lib/config/index.d.ts.map +1 -0
- package/lib/config/index.js +97 -2
- package/lib/constants/Collections.d.ts +44 -0
- package/lib/constants/Collections.d.ts.map +1 -0
- package/lib/constants/Collections.js +40 -0
- package/lib/constants/MetropolisConstants.d.ts +1 -0
- package/lib/constants/MetropolisConstants.d.ts.map +1 -0
- package/lib/constants/MetropolisConstants.js +15 -2
- package/lib/graphql/message.d.ts +1 -0
- package/lib/graphql/message.d.ts.map +1 -0
- package/lib/graphql/message.js +6 -4
- package/lib/graphql/notification.d.ts +1 -0
- package/lib/graphql/notification.d.ts.map +1 -0
- package/lib/graphql/notification.js +6 -4
- package/lib/graphql/session.d.ts +1 -0
- package/lib/graphql/session.d.ts.map +1 -0
- package/lib/graphql/session.js +4 -3
- package/lib/index.d.ts +45 -22
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +248 -2
- package/lib/stores/appStore.d.ts +1 -0
- package/lib/stores/appStore.d.ts.map +1 -0
- package/lib/stores/appStore.js +23 -2
- package/lib/stores/contentStore.d.ts +36 -0
- package/lib/stores/contentStore.d.ts.map +1 -0
- package/lib/stores/contentStore.js +139 -0
- package/lib/stores/conversationStore.d.ts +38 -0
- package/lib/stores/conversationStore.d.ts.map +1 -0
- package/lib/stores/conversationStore.js +117 -0
- package/lib/stores/eventStore.d.ts +4 -1
- package/lib/stores/eventStore.d.ts.map +1 -0
- package/lib/stores/eventStore.js +47 -2
- package/lib/stores/groupStore.d.ts +37 -0
- package/lib/stores/groupStore.d.ts.map +1 -0
- package/lib/stores/groupStore.js +68 -0
- package/lib/stores/imageStore.d.ts +6 -1
- package/lib/stores/imageStore.d.ts.map +1 -0
- package/lib/stores/imageStore.js +53 -2
- package/lib/stores/index.d.ts +20 -11
- package/lib/stores/index.d.ts.map +1 -0
- package/lib/stores/index.js +24 -2
- package/lib/stores/locationStore.d.ts +2 -1
- package/lib/stores/locationStore.d.ts.map +1 -0
- package/lib/stores/locationStore.js +42 -2
- package/lib/stores/messageStore.d.ts +19 -4
- package/lib/stores/messageStore.d.ts.map +1 -0
- package/lib/stores/messageStore.js +137 -2
- package/lib/stores/notificationStore.d.ts +33 -0
- package/lib/stores/notificationStore.d.ts.map +1 -0
- package/lib/stores/notificationStore.js +55 -0
- package/lib/stores/permissionStore.d.ts +37 -0
- package/lib/stores/permissionStore.d.ts.map +1 -0
- package/lib/stores/permissionStore.js +117 -0
- package/lib/stores/personaStore.d.ts +26 -0
- package/lib/stores/personaStore.d.ts.map +1 -0
- package/lib/stores/personaStore.js +118 -0
- package/lib/stores/postStore.d.ts +2 -1
- package/lib/stores/postStore.d.ts.map +1 -0
- package/lib/stores/postStore.js +53 -2
- package/lib/stores/reactionStore.d.ts +1 -0
- package/lib/stores/reactionStore.d.ts.map +1 -0
- package/lib/stores/reactionStore.js +28 -2
- package/lib/stores/subscriptionStore.d.ts +41 -0
- package/lib/stores/subscriptionStore.d.ts.map +1 -0
- package/lib/stores/subscriptionStore.js +85 -0
- package/lib/stores/tagStore.d.ts +8 -5
- package/lib/stores/tagStore.d.ts.map +1 -0
- package/lib/stores/tagStore.js +87 -2
- package/lib/stores/translationStore.d.ts +42 -0
- package/lib/stores/translationStore.d.ts.map +1 -0
- package/lib/stores/translationStore.js +122 -0
- package/lib/stores/userStore.d.ts +5 -4
- package/lib/stores/userStore.d.ts.map +1 -0
- package/lib/stores/userStore.js +319 -2
- package/lib/stores/videoStore.d.ts +39 -0
- package/lib/stores/videoStore.d.ts.map +1 -0
- package/lib/stores/videoStore.js +132 -0
- package/lib/stores/websocketStore.d.ts +1 -0
- package/lib/stores/websocketStore.d.ts.map +1 -0
- package/lib/stores/websocketStore.js +51 -2
- package/lib/types/apps.types.d.ts +17 -0
- package/lib/types/apps.types.d.ts.map +1 -0
- package/lib/types/apps.types.js +6 -0
- package/lib/types/common.types.d.ts +31 -0
- package/lib/types/common.types.d.ts.map +1 -0
- package/lib/types/common.types.js +6 -0
- package/lib/types/conversations.types.d.ts +17 -0
- package/lib/types/conversations.types.d.ts.map +1 -0
- package/lib/types/conversations.types.js +6 -0
- package/lib/types/edges.types.d.ts +38 -0
- package/lib/types/edges.types.d.ts.map +1 -0
- package/lib/types/edges.types.js +6 -0
- package/lib/types/groups.types.d.ts +19 -0
- package/lib/types/groups.types.d.ts.map +1 -0
- package/lib/types/groups.types.js +6 -0
- package/lib/types/index.d.ts +12 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +12 -0
- package/lib/types/subscriptions.types.d.ts +32 -0
- package/lib/types/subscriptions.types.d.ts.map +1 -0
- package/lib/types/subscriptions.types.js +6 -0
- package/lib/types/videos.types.d.ts +61 -0
- package/lib/types/videos.types.d.ts.map +1 -0
- package/lib/types/videos.types.js +6 -0
- package/lib/utils/MetropolisProvider.d.ts +39 -15
- package/lib/utils/MetropolisProvider.d.ts.map +1 -0
- package/lib/utils/MetropolisProvider.js +31 -2
- package/lib/utils/actionFactory.d.ts +21 -19
- package/lib/utils/actionFactory.d.ts.map +1 -0
- package/lib/utils/actionFactory.js +95 -2
- package/lib/utils/api.d.ts +5 -4
- package/lib/utils/api.d.ts.map +1 -0
- package/lib/utils/api.js +252 -6
- package/lib/utils/app.d.ts +1 -0
- package/lib/utils/app.d.ts.map +1 -0
- package/lib/utils/app.js +15 -2
- package/lib/utils/baseActionFactory.d.ts +5 -23
- package/lib/utils/baseActionFactory.d.ts.map +1 -0
- package/lib/utils/baseActionFactory.js +72 -2
- package/lib/utils/configUtils.d.ts +11 -0
- package/lib/utils/configUtils.d.ts.map +1 -0
- package/lib/utils/configUtils.js +20 -0
- package/lib/utils/dateUtils.d.ts +1 -0
- package/lib/utils/dateUtils.d.ts.map +1 -0
- package/lib/utils/dateUtils.js +11 -2
- package/lib/utils/file.d.ts +3 -1
- package/lib/utils/file.d.ts.map +1 -0
- package/lib/utils/file.js +156 -2
- package/lib/utils/i18n.d.ts +7 -0
- package/lib/utils/i18n.d.ts.map +1 -0
- package/lib/utils/i18n.js +96 -0
- package/lib/utils/index.d.ts +20 -0
- package/lib/utils/index.d.ts.map +1 -0
- package/lib/utils/index.js +26 -0
- package/lib/utils/location.d.ts +4 -3
- package/lib/utils/location.d.ts.map +1 -0
- package/lib/utils/location.js +82 -2
- package/lib/utils/permissionUtils.d.ts +22 -0
- package/lib/utils/permissionUtils.d.ts.map +1 -0
- package/lib/utils/permissionUtils.js +72 -0
- package/lib/utils/requestCache.d.ts +8 -0
- package/lib/utils/requestCache.d.ts.map +1 -0
- package/lib/utils/requestCache.js +53 -0
- package/lib/utils/session.d.ts +14 -0
- package/lib/utils/session.d.ts.map +1 -0
- package/lib/utils/session.js +107 -0
- package/lib/utils/useConversationTyping.d.ts +26 -0
- package/lib/utils/useConversationTyping.d.ts.map +1 -0
- package/lib/utils/useConversationTyping.js +117 -0
- package/lib/utils/useMetropolis.d.ts +67 -12
- package/lib/utils/useMetropolis.d.ts.map +1 -0
- package/lib/utils/useMetropolis.js +311 -2
- package/lib/utils/useTranslations.d.ts +16 -0
- package/lib/utils/useTranslations.d.ts.map +1 -0
- package/lib/utils/useTranslations.js +105 -0
- package/lib/utils/validatorFactory.d.ts +3 -2
- package/lib/utils/validatorFactory.d.ts.map +1 -0
- package/lib/utils/validatorFactory.js +95 -2
- package/package.json +58 -17
- package/temp_signin.ts +73 -0
- package/lib/actions/profileActions/profileActions.d.ts +0 -50
- package/lib/actions/profileActions/profileActions.js +0 -2
- package/lib/adapters/profileAdapter/profileAdapter.d.ts +0 -32
- package/lib/adapters/profileAdapter/profileAdapter.js +0 -2
- package/lib/stores/profileStore.d.ts +0 -24
- package/lib/stores/profileStore.js +0 -2
- /package/{FACTORY_PATTERN_GUIDE.md → factoryPatternGuide.md} +0 -0
|
@@ -0,0 +1,1367 @@
|
|
|
1
|
+
# MetropolisJS Connections Guide
|
|
2
|
+
|
|
3
|
+
Comprehensive guide for managing relationships between collections using connections, reactions, tags, conversations, and file associations.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Overview](#overview)
|
|
8
|
+
- [Connection Types](#connection-types)
|
|
9
|
+
- [Edge Types](#edge-types)
|
|
10
|
+
- [Connection Actions](#connection-actions)
|
|
11
|
+
- [Reaction Management](#reaction-management)
|
|
12
|
+
- [Tag Management](#tag-management)
|
|
13
|
+
- [File Associations](#file-associations)
|
|
14
|
+
- [Conversation Links](#conversation-links)
|
|
15
|
+
- [Advanced Patterns](#advanced-patterns)
|
|
16
|
+
- [Best Practices](#best-practices)
|
|
17
|
+
|
|
18
|
+
## Overview
|
|
19
|
+
|
|
20
|
+
MetropolisJS uses ArangoDB's edge collections to model relationships between documents. These relationships are managed through specialized actions that create, query, and remove edges between collections.
|
|
21
|
+
|
|
22
|
+
### Core Concepts
|
|
23
|
+
|
|
24
|
+
**Collections**: Documents like users, posts, groups (vertices in graph database)
|
|
25
|
+
**Edges**: Relationships between collections (edges in graph database)
|
|
26
|
+
**Connection Types**: Semantic meaning of relationships (admin, member, follower, etc.)
|
|
27
|
+
|
|
28
|
+
### Constants
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import {COLLECTIONS, EDGES, CONNECTION_TYPES, REACTION_TYPES} from '@nlabs/metropolisjs';
|
|
32
|
+
|
|
33
|
+
// Collection types
|
|
34
|
+
const collections = {
|
|
35
|
+
APPS: 'apps',
|
|
36
|
+
CONVERSATIONS: 'conversations',
|
|
37
|
+
FILES: 'files',
|
|
38
|
+
GROUPS: 'groups',
|
|
39
|
+
IMAGES: 'images',
|
|
40
|
+
MESSAGES: 'messages',
|
|
41
|
+
POSTS: 'posts',
|
|
42
|
+
PERSONAS: 'personas',
|
|
43
|
+
TAGS: 'tags',
|
|
44
|
+
USERS: 'users',
|
|
45
|
+
VIDEOS: 'videos'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Edge types
|
|
49
|
+
const edges = {
|
|
50
|
+
HAS_CONNECTION: 'hasConnection',
|
|
51
|
+
HAS_CONVERSATION: 'hasConversation',
|
|
52
|
+
HAS_FILE: 'hasFile',
|
|
53
|
+
HAS_REACTION: 'hasReaction',
|
|
54
|
+
IS_TAGGED: 'isTagged'
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Connection types
|
|
58
|
+
const connectionTypes = {
|
|
59
|
+
ADMIN: 'admin',
|
|
60
|
+
BLOCKED: 'blocked',
|
|
61
|
+
FOLLOWER: 'follower',
|
|
62
|
+
FOLLOWING: 'following',
|
|
63
|
+
MEMBER: 'member'
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Reaction types
|
|
67
|
+
const reactionTypes = {
|
|
68
|
+
DISLIKE: 'dislike',
|
|
69
|
+
LIKE: 'like',
|
|
70
|
+
PIN: 'pin',
|
|
71
|
+
RSVP: 'rsvp',
|
|
72
|
+
VIEW: 'view'
|
|
73
|
+
};
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Connection Types
|
|
77
|
+
|
|
78
|
+
Connection types define the semantic meaning of relationships between collections.
|
|
79
|
+
|
|
80
|
+
### ADMIN
|
|
81
|
+
|
|
82
|
+
Administrative privileges and ownership.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import {CONNECTION_TYPES, COLLECTIONS} from '@nlabs/metropolisjs';
|
|
86
|
+
|
|
87
|
+
// User is admin of group
|
|
88
|
+
await connectionActions.addConnection(
|
|
89
|
+
COLLECTIONS.USERS,
|
|
90
|
+
'user123',
|
|
91
|
+
COLLECTIONS.GROUPS,
|
|
92
|
+
'group456',
|
|
93
|
+
CONNECTION_TYPES.ADMIN,
|
|
94
|
+
{
|
|
95
|
+
permissions: ['manage_members', 'edit_settings', 'delete_posts'],
|
|
96
|
+
grantedAt: Date.now()
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Check if user is admin
|
|
101
|
+
const connections = await connectionActions.getConnections(
|
|
102
|
+
COLLECTIONS.USERS,
|
|
103
|
+
'user123',
|
|
104
|
+
COLLECTIONS.GROUPS,
|
|
105
|
+
{connectionType: CONNECTION_TYPES.ADMIN}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const isAdmin = connections.some(conn => conn.toId === 'group456');
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### MEMBER
|
|
112
|
+
|
|
113
|
+
Standard membership in groups or organizations.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// Add user to group
|
|
117
|
+
await connectionActions.addConnection(
|
|
118
|
+
COLLECTIONS.USERS,
|
|
119
|
+
'user123',
|
|
120
|
+
COLLECTIONS.GROUPS,
|
|
121
|
+
'group456',
|
|
122
|
+
CONNECTION_TYPES.MEMBER,
|
|
123
|
+
{
|
|
124
|
+
joinedAt: Date.now(),
|
|
125
|
+
role: 'contributor'
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// List group members
|
|
130
|
+
const members = await connectionActions.getConnections(
|
|
131
|
+
COLLECTIONS.GROUPS,
|
|
132
|
+
'group456',
|
|
133
|
+
COLLECTIONS.USERS,
|
|
134
|
+
{connectionType: CONNECTION_TYPES.MEMBER}
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
console.log(`Group has ${members.length} members`);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### FOLLOWER / FOLLOWING
|
|
141
|
+
|
|
142
|
+
Social following relationships.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// User follows another user
|
|
146
|
+
await connectionActions.addConnection(
|
|
147
|
+
COLLECTIONS.USERS,
|
|
148
|
+
'user123', // follower
|
|
149
|
+
COLLECTIONS.USERS,
|
|
150
|
+
'user456', // following
|
|
151
|
+
CONNECTION_TYPES.FOLLOWING
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// This creates complementary relationship
|
|
155
|
+
// user123 FOLLOWING user456
|
|
156
|
+
// user456 has FOLLOWER user123
|
|
157
|
+
|
|
158
|
+
// Get users I'm following
|
|
159
|
+
const following = await connectionActions.getConnections(
|
|
160
|
+
COLLECTIONS.USERS,
|
|
161
|
+
'user123',
|
|
162
|
+
COLLECTIONS.USERS,
|
|
163
|
+
{connectionType: CONNECTION_TYPES.FOLLOWING}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Get my followers
|
|
167
|
+
const followers = await connectionActions.getConnections(
|
|
168
|
+
COLLECTIONS.USERS,
|
|
169
|
+
'user123',
|
|
170
|
+
COLLECTIONS.USERS,
|
|
171
|
+
{connectionType: CONNECTION_TYPES.FOLLOWER}
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
console.log(`Following ${following.length}, Followers ${followers.length}`);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### BLOCKED
|
|
178
|
+
|
|
179
|
+
Block relationships for moderation.
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// Block a user
|
|
183
|
+
await connectionActions.addConnection(
|
|
184
|
+
COLLECTIONS.USERS,
|
|
185
|
+
'user123',
|
|
186
|
+
COLLECTIONS.USERS,
|
|
187
|
+
'user456',
|
|
188
|
+
CONNECTION_TYPES.BLOCKED,
|
|
189
|
+
{
|
|
190
|
+
reason: 'spam',
|
|
191
|
+
blockedAt: Date.now()
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Check if blocked
|
|
196
|
+
const blockedUsers = await connectionActions.getConnections(
|
|
197
|
+
COLLECTIONS.USERS,
|
|
198
|
+
'user123',
|
|
199
|
+
COLLECTIONS.USERS,
|
|
200
|
+
{connectionType: CONNECTION_TYPES.BLOCKED}
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const isBlocked = blockedUsers.some(conn => conn.toId === 'user456');
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Edge Types
|
|
207
|
+
|
|
208
|
+
Edge types define different categories of relationships in the system.
|
|
209
|
+
|
|
210
|
+
### HAS_CONNECTION (hasConnection)
|
|
211
|
+
|
|
212
|
+
General connections between any collections.
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import {EDGES, COLLECTIONS, CONNECTION_TYPES} from '@nlabs/metropolisjs';
|
|
216
|
+
|
|
217
|
+
const connectionActions = createConnectionActions(flux);
|
|
218
|
+
|
|
219
|
+
// User connects to group
|
|
220
|
+
await connectionActions.addConnection(
|
|
221
|
+
COLLECTIONS.USERS,
|
|
222
|
+
'user123',
|
|
223
|
+
COLLECTIONS.GROUPS,
|
|
224
|
+
'group456',
|
|
225
|
+
CONNECTION_TYPES.MEMBER
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Post connects to group
|
|
229
|
+
await connectionActions.addConnection(
|
|
230
|
+
COLLECTIONS.POSTS,
|
|
231
|
+
'post789',
|
|
232
|
+
COLLECTIONS.GROUPS,
|
|
233
|
+
'group456',
|
|
234
|
+
CONNECTION_TYPES.MEMBER,
|
|
235
|
+
{publishedAt: Date.now()}
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// Get all group connections
|
|
239
|
+
const groupConnections = await connectionActions.getConnections(
|
|
240
|
+
COLLECTIONS.GROUPS,
|
|
241
|
+
'group456'
|
|
242
|
+
);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### HAS_REACTION (hasReaction)
|
|
246
|
+
|
|
247
|
+
Reactions like likes, views, pins, etc.
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import {REACTION_TYPES} from '@nlabs/metropolisjs';
|
|
251
|
+
|
|
252
|
+
const reactionActions = createReactionActions(flux);
|
|
253
|
+
|
|
254
|
+
// Add like to post
|
|
255
|
+
await reactionActions.addReaction(
|
|
256
|
+
'post123',
|
|
257
|
+
COLLECTIONS.POSTS,
|
|
258
|
+
{
|
|
259
|
+
name: REACTION_TYPES.LIKE
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Add view count
|
|
264
|
+
await reactionActions.addReaction(
|
|
265
|
+
'post123',
|
|
266
|
+
COLLECTIONS.POSTS,
|
|
267
|
+
{
|
|
268
|
+
name: REACTION_TYPES.VIEW,
|
|
269
|
+
value: 1
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Get reaction count
|
|
274
|
+
const likeCount = await reactionActions.getReactionCount(
|
|
275
|
+
'post123',
|
|
276
|
+
COLLECTIONS.POSTS,
|
|
277
|
+
REACTION_TYPES.LIKE
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// Check if user has reacted
|
|
281
|
+
const hasLiked = await reactionActions.hasReaction(
|
|
282
|
+
'post123',
|
|
283
|
+
COLLECTIONS.POSTS,
|
|
284
|
+
REACTION_TYPES.LIKE,
|
|
285
|
+
'outbound'
|
|
286
|
+
);
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### IS_TAGGED (isTagged)
|
|
290
|
+
|
|
291
|
+
Tag associations for categorization.
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
const tagActions = createTagActions(flux);
|
|
295
|
+
|
|
296
|
+
// Tag a post
|
|
297
|
+
await tagActions.tagItem(
|
|
298
|
+
'post123',
|
|
299
|
+
COLLECTIONS.POSTS,
|
|
300
|
+
['javascript', 'tutorial', 'beginner']
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// Get items by tag
|
|
304
|
+
const taggedPosts = await tagActions.itemsByTag(
|
|
305
|
+
'javascript',
|
|
306
|
+
COLLECTIONS.POSTS
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Get tags for item
|
|
310
|
+
const postTags = await tagActions.tagsByItem(
|
|
311
|
+
'post123',
|
|
312
|
+
COLLECTIONS.POSTS
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// Remove tag
|
|
316
|
+
await tagActions.untagItem(
|
|
317
|
+
'post123',
|
|
318
|
+
COLLECTIONS.POSTS,
|
|
319
|
+
'beginner'
|
|
320
|
+
);
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### HAS_FILE (hasFile)
|
|
324
|
+
|
|
325
|
+
File attachments to any collection.
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// Attach files to post
|
|
329
|
+
await connectionActions.addConnection(
|
|
330
|
+
COLLECTIONS.POSTS,
|
|
331
|
+
'post123',
|
|
332
|
+
COLLECTIONS.FILES,
|
|
333
|
+
'file456',
|
|
334
|
+
CONNECTION_TYPES.MEMBER,
|
|
335
|
+
{
|
|
336
|
+
order: 1,
|
|
337
|
+
caption: 'Screenshot'
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// Attach image
|
|
342
|
+
await connectionActions.addConnection(
|
|
343
|
+
COLLECTIONS.POSTS,
|
|
344
|
+
'post123',
|
|
345
|
+
COLLECTIONS.IMAGES,
|
|
346
|
+
'image789',
|
|
347
|
+
CONNECTION_TYPES.MEMBER,
|
|
348
|
+
{
|
|
349
|
+
order: 2,
|
|
350
|
+
caption: 'Header image'
|
|
351
|
+
}
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// Get post files
|
|
355
|
+
const files = await connectionActions.getConnections(
|
|
356
|
+
COLLECTIONS.POSTS,
|
|
357
|
+
'post123',
|
|
358
|
+
COLLECTIONS.FILES
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// Get post images
|
|
362
|
+
const images = await connectionActions.getConnections(
|
|
363
|
+
COLLECTIONS.POSTS,
|
|
364
|
+
'post123',
|
|
365
|
+
COLLECTIONS.IMAGES
|
|
366
|
+
);
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### HAS_CONVERSATION (hasConversation)
|
|
370
|
+
|
|
371
|
+
Links items to conversations for commenting/discussion.
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
// Create conversation for post
|
|
375
|
+
const conversation = await conversationActions.add({
|
|
376
|
+
type: 'post_comments',
|
|
377
|
+
isGroup: true
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Link post to conversation
|
|
381
|
+
await connectionActions.addConnection(
|
|
382
|
+
COLLECTIONS.POSTS,
|
|
383
|
+
'post123',
|
|
384
|
+
COLLECTIONS.CONVERSATIONS,
|
|
385
|
+
conversation.conversationId,
|
|
386
|
+
CONNECTION_TYPES.MEMBER,
|
|
387
|
+
{
|
|
388
|
+
purpose: 'comments',
|
|
389
|
+
createdAt: Date.now()
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
// Get post conversation
|
|
394
|
+
const postConversations = await connectionActions.getConnections(
|
|
395
|
+
COLLECTIONS.POSTS,
|
|
396
|
+
'post123',
|
|
397
|
+
COLLECTIONS.CONVERSATIONS
|
|
398
|
+
);
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## Connection Actions
|
|
402
|
+
|
|
403
|
+
### Creating Connections
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
import {createConnectionActions} from '@nlabs/metropolisjs';
|
|
407
|
+
|
|
408
|
+
const connectionActions = createConnectionActions(flux);
|
|
409
|
+
|
|
410
|
+
interface ConnectionActions {
|
|
411
|
+
addConnection: (
|
|
412
|
+
fromType: CollectionType,
|
|
413
|
+
fromId: string,
|
|
414
|
+
toType: CollectionType,
|
|
415
|
+
toId: string,
|
|
416
|
+
connectionType?: ConnectionType,
|
|
417
|
+
metadata?: Record<string, any>
|
|
418
|
+
) => Promise<ConnectionEdge>;
|
|
419
|
+
|
|
420
|
+
getConnections: (
|
|
421
|
+
fromType: CollectionType,
|
|
422
|
+
fromId: string,
|
|
423
|
+
toType?: CollectionType,
|
|
424
|
+
filters?: Record<string, any>
|
|
425
|
+
) => Promise<ConnectionEdge[]>;
|
|
426
|
+
|
|
427
|
+
removeConnection: (
|
|
428
|
+
fromType: CollectionType,
|
|
429
|
+
fromId: string,
|
|
430
|
+
toType: CollectionType,
|
|
431
|
+
toId: string
|
|
432
|
+
) => Promise<boolean>;
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Connection Edge Structure
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
interface ConnectionEdge {
|
|
440
|
+
_from?: string; // Source document ID (e.g., 'users/123')
|
|
441
|
+
_to?: string; // Target document ID (e.g., 'groups/456')
|
|
442
|
+
fromId?: string; // Source ID ('123')
|
|
443
|
+
fromType?: CollectionType; // Source collection ('users')
|
|
444
|
+
toId?: string; // Target ID ('456')
|
|
445
|
+
toType?: CollectionType; // Target collection ('groups')
|
|
446
|
+
connectionType?: ConnectionType; // Connection type ('member', 'admin', etc.)
|
|
447
|
+
status?: string; // Connection status
|
|
448
|
+
metadata?: Record<string, any>; // Additional data
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Examples
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
// Add connection with metadata
|
|
456
|
+
const connection = await connectionActions.addConnection(
|
|
457
|
+
COLLECTIONS.USERS,
|
|
458
|
+
'user123',
|
|
459
|
+
COLLECTIONS.GROUPS,
|
|
460
|
+
'group456',
|
|
461
|
+
CONNECTION_TYPES.MEMBER,
|
|
462
|
+
{
|
|
463
|
+
joinedAt: Date.now(),
|
|
464
|
+
invitedBy: 'user789',
|
|
465
|
+
role: 'contributor',
|
|
466
|
+
permissions: ['post', 'comment']
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
console.log('Connection created:', connection);
|
|
471
|
+
|
|
472
|
+
// Query connections with filters
|
|
473
|
+
const adminConnections = await connectionActions.getConnections(
|
|
474
|
+
COLLECTIONS.USERS,
|
|
475
|
+
'user123',
|
|
476
|
+
COLLECTIONS.GROUPS,
|
|
477
|
+
{
|
|
478
|
+
connectionType: CONNECTION_TYPES.ADMIN,
|
|
479
|
+
status: 'active'
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
// Remove connection
|
|
484
|
+
await connectionActions.removeConnection(
|
|
485
|
+
COLLECTIONS.USERS,
|
|
486
|
+
'user123',
|
|
487
|
+
COLLECTIONS.GROUPS,
|
|
488
|
+
'group456'
|
|
489
|
+
);
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Reaction Management
|
|
493
|
+
|
|
494
|
+
Reactions provide engagement metrics for content.
|
|
495
|
+
|
|
496
|
+
### Reaction Types
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
const reactionTypes = {
|
|
500
|
+
LIKE: 'like', // Positive feedback
|
|
501
|
+
DISLIKE: 'dislike', // Negative feedback
|
|
502
|
+
VIEW: 'view', // View count
|
|
503
|
+
PIN: 'pin', // Pin/bookmark
|
|
504
|
+
RSVP: 'rsvp' // Event RSVP
|
|
505
|
+
};
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Reaction Actions
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
import {createReactionActions} from '@nlabs/metropolisjs';
|
|
512
|
+
|
|
513
|
+
const reactionActions = createReactionActions(flux);
|
|
514
|
+
|
|
515
|
+
interface ReactionActions {
|
|
516
|
+
addReaction: (itemId, itemType, reaction, props?) => Promise<Reaction>;
|
|
517
|
+
deleteReaction: (itemId, itemType, reactionName, props?) => Promise<Reaction>;
|
|
518
|
+
getReactionCount: (itemId, itemType, reactionName) => Promise<number>;
|
|
519
|
+
hasReaction: (itemId, itemType, reactionName, direction) => Promise<boolean>;
|
|
520
|
+
abbreviateCount: (count) => string;
|
|
521
|
+
updateReactionAdapter: (adapter) => void;
|
|
522
|
+
updateReactionAdapterOptions: (options) => void;
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Reaction Examples
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
// Like a post
|
|
530
|
+
await reactionActions.addReaction(
|
|
531
|
+
'post123',
|
|
532
|
+
COLLECTIONS.POSTS,
|
|
533
|
+
{name: REACTION_TYPES.LIKE}
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
// Unlike a post
|
|
537
|
+
await reactionActions.deleteReaction(
|
|
538
|
+
'post123',
|
|
539
|
+
COLLECTIONS.POSTS,
|
|
540
|
+
REACTION_TYPES.LIKE
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
// Track view
|
|
544
|
+
await reactionActions.addReaction(
|
|
545
|
+
'post123',
|
|
546
|
+
COLLECTIONS.POSTS,
|
|
547
|
+
{
|
|
548
|
+
name: REACTION_TYPES.VIEW,
|
|
549
|
+
value: 1
|
|
550
|
+
}
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
// Pin content
|
|
554
|
+
await reactionActions.addReaction(
|
|
555
|
+
'post123',
|
|
556
|
+
COLLECTIONS.POSTS,
|
|
557
|
+
{name: REACTION_TYPES.PIN}
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
// RSVP to event
|
|
561
|
+
await reactionActions.addReaction(
|
|
562
|
+
'event456',
|
|
563
|
+
COLLECTIONS.POSTS,
|
|
564
|
+
{
|
|
565
|
+
name: REACTION_TYPES.RSVP,
|
|
566
|
+
value: 'attending'
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
// Get reaction counts
|
|
571
|
+
const likes = await reactionActions.getReactionCount(
|
|
572
|
+
'post123',
|
|
573
|
+
COLLECTIONS.POSTS,
|
|
574
|
+
REACTION_TYPES.LIKE
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
const views = await reactionActions.getReactionCount(
|
|
578
|
+
'post123',
|
|
579
|
+
COLLECTIONS.POSTS,
|
|
580
|
+
REACTION_TYPES.VIEW
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
// Check if current user has liked
|
|
584
|
+
const hasLiked = await reactionActions.hasReaction(
|
|
585
|
+
'post123',
|
|
586
|
+
COLLECTIONS.POSTS,
|
|
587
|
+
REACTION_TYPES.LIKE,
|
|
588
|
+
'outbound'
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
// Display abbreviated counts
|
|
592
|
+
console.log(reactionActions.abbreviateCount(1234)); // "1.2k"
|
|
593
|
+
console.log(reactionActions.abbreviateCount(1500000)); // "1.5m"
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### Reaction Component Example
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
const PostReactions: React.FC<{postId: string}> = ({postId}) => {
|
|
600
|
+
const [likes, setLikes] = React.useState(0);
|
|
601
|
+
const [hasLiked, setHasLiked] = React.useState(false);
|
|
602
|
+
|
|
603
|
+
React.useEffect(() => {
|
|
604
|
+
const loadReactions = async () => {
|
|
605
|
+
const count = await reactionActions.getReactionCount(
|
|
606
|
+
postId,
|
|
607
|
+
COLLECTIONS.POSTS,
|
|
608
|
+
REACTION_TYPES.LIKE
|
|
609
|
+
);
|
|
610
|
+
setLikes(count);
|
|
611
|
+
|
|
612
|
+
const liked = await reactionActions.hasReaction(
|
|
613
|
+
postId,
|
|
614
|
+
COLLECTIONS.POSTS,
|
|
615
|
+
REACTION_TYPES.LIKE,
|
|
616
|
+
'outbound'
|
|
617
|
+
);
|
|
618
|
+
setHasLiked(liked);
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
loadReactions();
|
|
622
|
+
}, [postId]);
|
|
623
|
+
|
|
624
|
+
const handleLike = async () => {
|
|
625
|
+
if (hasLiked) {
|
|
626
|
+
await reactionActions.deleteReaction(
|
|
627
|
+
postId,
|
|
628
|
+
COLLECTIONS.POSTS,
|
|
629
|
+
REACTION_TYPES.LIKE
|
|
630
|
+
);
|
|
631
|
+
setLikes(likes - 1);
|
|
632
|
+
setHasLiked(false);
|
|
633
|
+
} else {
|
|
634
|
+
await reactionActions.addReaction(
|
|
635
|
+
postId,
|
|
636
|
+
COLLECTIONS.POSTS,
|
|
637
|
+
{name: REACTION_TYPES.LIKE}
|
|
638
|
+
);
|
|
639
|
+
setLikes(likes + 1);
|
|
640
|
+
setHasLiked(true);
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
return (
|
|
645
|
+
<button onClick={handleLike}>
|
|
646
|
+
{hasLiked ? '❤️' : '🤍'} {reactionActions.abbreviateCount(likes)}
|
|
647
|
+
</button>
|
|
648
|
+
);
|
|
649
|
+
};
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
## Tag Management
|
|
653
|
+
|
|
654
|
+
Tags provide flexible categorization across collections.
|
|
655
|
+
|
|
656
|
+
### Tag Actions
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
import {createTagActions} from '@nlabs/metropolisjs';
|
|
660
|
+
|
|
661
|
+
const tagActions = createTagActions(flux);
|
|
662
|
+
|
|
663
|
+
// Tag operations
|
|
664
|
+
await tagActions.tagItem(itemId, collectionType, tags);
|
|
665
|
+
await tagActions.untagItem(itemId, collectionType, tagName);
|
|
666
|
+
const items = await tagActions.itemsByTag(tagName, collectionType);
|
|
667
|
+
const tags = await tagActions.tagsByItem(itemId, collectionType);
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
### Tag Examples
|
|
671
|
+
|
|
672
|
+
```typescript
|
|
673
|
+
// Tag a post
|
|
674
|
+
await tagActions.tagItem(
|
|
675
|
+
'post123',
|
|
676
|
+
COLLECTIONS.POSTS,
|
|
677
|
+
['javascript', 'tutorial', 'react']
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
// Tag a group
|
|
681
|
+
await tagActions.tagItem(
|
|
682
|
+
'group456',
|
|
683
|
+
COLLECTIONS.GROUPS,
|
|
684
|
+
['technology', 'programming', 'web-dev']
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
// Get all posts with tag
|
|
688
|
+
const jsPosts = await tagActions.itemsByTag(
|
|
689
|
+
'javascript',
|
|
690
|
+
COLLECTIONS.POSTS
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
// Get all tags for post
|
|
694
|
+
const postTags = await tagActions.tagsByItem(
|
|
695
|
+
'post123',
|
|
696
|
+
COLLECTIONS.POSTS
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
console.log('Tags:', postTags.map(t => t.name).join(', '));
|
|
700
|
+
|
|
701
|
+
// Remove specific tag
|
|
702
|
+
await tagActions.untagItem(
|
|
703
|
+
'post123',
|
|
704
|
+
COLLECTIONS.POSTS,
|
|
705
|
+
'tutorial'
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
// Update tags (remove all and add new)
|
|
709
|
+
const currentTags = await tagActions.tagsByItem('post123', COLLECTIONS.POSTS);
|
|
710
|
+
await Promise.all(
|
|
711
|
+
currentTags.map(tag =>
|
|
712
|
+
tagActions.untagItem('post123', COLLECTIONS.POSTS, tag.name)
|
|
713
|
+
)
|
|
714
|
+
);
|
|
715
|
+
await tagActions.tagItem('post123', COLLECTIONS.POSTS, ['updated', 'tags']);
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### Tag Search Component
|
|
719
|
+
|
|
720
|
+
```typescript
|
|
721
|
+
const TagSearch: React.FC = () => {
|
|
722
|
+
const [selectedTag, setSelectedTag] = React.useState('');
|
|
723
|
+
const [posts, setPosts] = React.useState([]);
|
|
724
|
+
|
|
725
|
+
const searchByTag = async (tagName: string) => {
|
|
726
|
+
setSelectedTag(tagName);
|
|
727
|
+
const results = await tagActions.itemsByTag(
|
|
728
|
+
tagName,
|
|
729
|
+
COLLECTIONS.POSTS
|
|
730
|
+
);
|
|
731
|
+
setPosts(results);
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
return (
|
|
735
|
+
<div>
|
|
736
|
+
<input
|
|
737
|
+
type="text"
|
|
738
|
+
placeholder="Search by tag"
|
|
739
|
+
onChange={(e) => searchByTag(e.target.value)}
|
|
740
|
+
/>
|
|
741
|
+
<div>
|
|
742
|
+
{posts.map(post => (
|
|
743
|
+
<div key={post.postId}>{post.name}</div>
|
|
744
|
+
))}
|
|
745
|
+
</div>
|
|
746
|
+
</div>
|
|
747
|
+
);
|
|
748
|
+
};
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
## File Associations
|
|
752
|
+
|
|
753
|
+
Link files, images, and videos to any collection.
|
|
754
|
+
|
|
755
|
+
### Attaching Files
|
|
756
|
+
|
|
757
|
+
```typescript
|
|
758
|
+
// Attach file to post
|
|
759
|
+
await connectionActions.addConnection(
|
|
760
|
+
COLLECTIONS.POSTS,
|
|
761
|
+
'post123',
|
|
762
|
+
COLLECTIONS.FILES,
|
|
763
|
+
'file456',
|
|
764
|
+
CONNECTION_TYPES.MEMBER,
|
|
765
|
+
{
|
|
766
|
+
order: 1,
|
|
767
|
+
caption: 'Project document',
|
|
768
|
+
fileType: 'application/pdf'
|
|
769
|
+
}
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
// Attach multiple images
|
|
773
|
+
const imageIds = ['img1', 'img2', 'img3'];
|
|
774
|
+
await Promise.all(
|
|
775
|
+
imageIds.map((imageId, index) =>
|
|
776
|
+
connectionActions.addConnection(
|
|
777
|
+
COLLECTIONS.POSTS,
|
|
778
|
+
'post123',
|
|
779
|
+
COLLECTIONS.IMAGES,
|
|
780
|
+
imageId,
|
|
781
|
+
CONNECTION_TYPES.MEMBER,
|
|
782
|
+
{
|
|
783
|
+
order: index,
|
|
784
|
+
caption: `Image ${index + 1}`
|
|
785
|
+
}
|
|
786
|
+
)
|
|
787
|
+
)
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
// Attach video
|
|
791
|
+
await connectionActions.addConnection(
|
|
792
|
+
COLLECTIONS.POSTS,
|
|
793
|
+
'post123',
|
|
794
|
+
COLLECTIONS.VIDEOS,
|
|
795
|
+
'video789',
|
|
796
|
+
CONNECTION_TYPES.MEMBER,
|
|
797
|
+
{
|
|
798
|
+
order: 0,
|
|
799
|
+
caption: 'Tutorial video',
|
|
800
|
+
isFeatured: true
|
|
801
|
+
}
|
|
802
|
+
);
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
### Retrieving Files
|
|
806
|
+
|
|
807
|
+
```typescript
|
|
808
|
+
// Get all post files
|
|
809
|
+
const files = await connectionActions.getConnections(
|
|
810
|
+
COLLECTIONS.POSTS,
|
|
811
|
+
'post123',
|
|
812
|
+
COLLECTIONS.FILES
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
// Get images ordered by metadata
|
|
816
|
+
const images = await connectionActions.getConnections(
|
|
817
|
+
COLLECTIONS.POSTS,
|
|
818
|
+
'post123',
|
|
819
|
+
COLLECTIONS.IMAGES
|
|
820
|
+
);
|
|
821
|
+
const sortedImages = images.sort((a, b) =>
|
|
822
|
+
(a.metadata?.order || 0) - (b.metadata?.order || 0)
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
// Get all media (images + videos)
|
|
826
|
+
const allFiles = await connectionActions.getConnections(
|
|
827
|
+
COLLECTIONS.POSTS,
|
|
828
|
+
'post123'
|
|
829
|
+
);
|
|
830
|
+
const media = allFiles.filter(conn =>
|
|
831
|
+
conn.toType === COLLECTIONS.IMAGES ||
|
|
832
|
+
conn.toType === COLLECTIONS.VIDEOS
|
|
833
|
+
);
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
### File Upload Example
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
const uploadAndAttach = async (
|
|
840
|
+
file: File,
|
|
841
|
+
postId: string
|
|
842
|
+
): Promise<void> => {
|
|
843
|
+
// 1. Upload file (implement your upload logic)
|
|
844
|
+
const uploadedFile = await uploadFileToServer(file);
|
|
845
|
+
|
|
846
|
+
// 2. Create file metadata
|
|
847
|
+
const fileMetadata = await fileActions.add({
|
|
848
|
+
fileName: file.name,
|
|
849
|
+
fileType: file.type,
|
|
850
|
+
size: file.size,
|
|
851
|
+
url: uploadedFile.url
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
// 3. Attach to post
|
|
855
|
+
await connectionActions.addConnection(
|
|
856
|
+
COLLECTIONS.POSTS,
|
|
857
|
+
postId,
|
|
858
|
+
COLLECTIONS.FILES,
|
|
859
|
+
fileMetadata.fileId,
|
|
860
|
+
CONNECTION_TYPES.MEMBER,
|
|
861
|
+
{
|
|
862
|
+
uploadedAt: Date.now(),
|
|
863
|
+
caption: ''
|
|
864
|
+
}
|
|
865
|
+
);
|
|
866
|
+
};
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
## Conversation Links
|
|
870
|
+
|
|
871
|
+
Link conversations for comments, discussions, and messaging.
|
|
872
|
+
|
|
873
|
+
### Creating Conversation Links
|
|
874
|
+
|
|
875
|
+
```typescript
|
|
876
|
+
// Create conversation for post comments
|
|
877
|
+
const conversation = await conversationActions.add({
|
|
878
|
+
type: 'post_comments',
|
|
879
|
+
isGroup: true,
|
|
880
|
+
name: 'Post Discussion'
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
// Link to post
|
|
884
|
+
await connectionActions.addConnection(
|
|
885
|
+
COLLECTIONS.POSTS,
|
|
886
|
+
'post123',
|
|
887
|
+
COLLECTIONS.CONVERSATIONS,
|
|
888
|
+
conversation.conversationId,
|
|
889
|
+
CONNECTION_TYPES.MEMBER,
|
|
890
|
+
{
|
|
891
|
+
purpose: 'comments',
|
|
892
|
+
createdAt: Date.now()
|
|
893
|
+
}
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
// Get post conversation
|
|
897
|
+
const postConversations = await connectionActions.getConnections(
|
|
898
|
+
COLLECTIONS.POSTS,
|
|
899
|
+
'post123',
|
|
900
|
+
COLLECTIONS.CONVERSATIONS
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
const commentConversation = postConversations[0];
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
### Comment System Example
|
|
907
|
+
|
|
908
|
+
```typescript
|
|
909
|
+
const CommentSection: React.FC<{postId: string}> = ({postId}) => {
|
|
910
|
+
const [conversationId, setConversationId] = React.useState('');
|
|
911
|
+
const [messages, setMessages] = React.useState([]);
|
|
912
|
+
|
|
913
|
+
React.useEffect(() => {
|
|
914
|
+
const loadComments = async () => {
|
|
915
|
+
// Get post conversation
|
|
916
|
+
const conversations = await connectionActions.getConnections(
|
|
917
|
+
COLLECTIONS.POSTS,
|
|
918
|
+
postId,
|
|
919
|
+
COLLECTIONS.CONVERSATIONS
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
if (conversations.length === 0) {
|
|
923
|
+
// Create conversation if doesn't exist
|
|
924
|
+
const conv = await conversationActions.add({
|
|
925
|
+
type: 'post_comments',
|
|
926
|
+
isGroup: true
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
await connectionActions.addConnection(
|
|
930
|
+
COLLECTIONS.POSTS,
|
|
931
|
+
postId,
|
|
932
|
+
COLLECTIONS.CONVERSATIONS,
|
|
933
|
+
conv.conversationId,
|
|
934
|
+
CONNECTION_TYPES.MEMBER
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
setConversationId(conv.conversationId);
|
|
938
|
+
} else {
|
|
939
|
+
setConversationId(conversations[0].toId);
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
loadComments();
|
|
944
|
+
}, [postId]);
|
|
945
|
+
|
|
946
|
+
React.useEffect(() => {
|
|
947
|
+
if (conversationId) {
|
|
948
|
+
const loadMessages = async () => {
|
|
949
|
+
const msgs = await messageActions.listByConversation(
|
|
950
|
+
conversationId,
|
|
951
|
+
0,
|
|
952
|
+
50
|
|
953
|
+
);
|
|
954
|
+
setMessages(msgs);
|
|
955
|
+
};
|
|
956
|
+
loadMessages();
|
|
957
|
+
}
|
|
958
|
+
}, [conversationId]);
|
|
959
|
+
|
|
960
|
+
const addComment = async (content: string) => {
|
|
961
|
+
await messageActions.add({
|
|
962
|
+
conversationId,
|
|
963
|
+
content,
|
|
964
|
+
type: 'text'
|
|
965
|
+
});
|
|
966
|
+
// Reload messages
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
return (
|
|
970
|
+
<div>
|
|
971
|
+
{messages.map(msg => (
|
|
972
|
+
<div key={msg.messageId}>{msg.content}</div>
|
|
973
|
+
))}
|
|
974
|
+
<textarea onBlur={(e) => addComment(e.target.value)} />
|
|
975
|
+
</div>
|
|
976
|
+
);
|
|
977
|
+
};
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
## Advanced Patterns
|
|
981
|
+
|
|
982
|
+
### Multi-hop Relationships
|
|
983
|
+
|
|
984
|
+
```typescript
|
|
985
|
+
// Get all posts from groups user is member of
|
|
986
|
+
const getUserGroupPosts = async (userId: string) => {
|
|
987
|
+
// 1. Get user's groups
|
|
988
|
+
const userGroups = await connectionActions.getConnections(
|
|
989
|
+
COLLECTIONS.USERS,
|
|
990
|
+
userId,
|
|
991
|
+
COLLECTIONS.GROUPS,
|
|
992
|
+
{connectionType: CONNECTION_TYPES.MEMBER}
|
|
993
|
+
);
|
|
994
|
+
|
|
995
|
+
// 2. Get posts from each group
|
|
996
|
+
const groupPosts = await Promise.all(
|
|
997
|
+
userGroups.map(group =>
|
|
998
|
+
connectionActions.getConnections(
|
|
999
|
+
COLLECTIONS.GROUPS,
|
|
1000
|
+
group.toId,
|
|
1001
|
+
COLLECTIONS.POSTS
|
|
1002
|
+
)
|
|
1003
|
+
)
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
// 3. Flatten results
|
|
1007
|
+
return groupPosts.flat();
|
|
1008
|
+
};
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
### Permission Checking
|
|
1012
|
+
|
|
1013
|
+
```typescript
|
|
1014
|
+
const hasPermission = async (
|
|
1015
|
+
userId: string,
|
|
1016
|
+
groupId: string,
|
|
1017
|
+
permission: 'admin' | 'member'
|
|
1018
|
+
): Promise<boolean> => {
|
|
1019
|
+
const connections = await connectionActions.getConnections(
|
|
1020
|
+
COLLECTIONS.USERS,
|
|
1021
|
+
userId,
|
|
1022
|
+
COLLECTIONS.GROUPS
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
const groupConnection = connections.find(
|
|
1026
|
+
conn => conn.toId === groupId
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
if (!groupConnection) return false;
|
|
1030
|
+
|
|
1031
|
+
if (permission === 'admin') {
|
|
1032
|
+
return groupConnection.connectionType === CONNECTION_TYPES.ADMIN;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return groupConnection.connectionType === CONNECTION_TYPES.ADMIN ||
|
|
1036
|
+
groupConnection.connectionType === CONNECTION_TYPES.MEMBER;
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
// Usage
|
|
1040
|
+
const canManageGroup = await hasPermission('user123', 'group456', 'admin');
|
|
1041
|
+
const canViewGroup = await hasPermission('user123', 'group456', 'member');
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
### Social Feed
|
|
1045
|
+
|
|
1046
|
+
```typescript
|
|
1047
|
+
const getSocialFeed = async (userId: string) => {
|
|
1048
|
+
// Get users I'm following
|
|
1049
|
+
const following = await connectionActions.getConnections(
|
|
1050
|
+
COLLECTIONS.USERS,
|
|
1051
|
+
userId,
|
|
1052
|
+
COLLECTIONS.USERS,
|
|
1053
|
+
{connectionType: CONNECTION_TYPES.FOLLOWING}
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
// Get posts from followed users
|
|
1057
|
+
const followedUserIds = following.map(conn => conn.toId);
|
|
1058
|
+
const feedPosts = await Promise.all(
|
|
1059
|
+
followedUserIds.map(async (followedUserId) => {
|
|
1060
|
+
// Query posts by user
|
|
1061
|
+
const userPosts = await postActions.listByLatest(0, 10);
|
|
1062
|
+
return userPosts.filter(post => post.userId === followedUserId);
|
|
1063
|
+
})
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
// Combine and sort by date
|
|
1067
|
+
return feedPosts
|
|
1068
|
+
.flat()
|
|
1069
|
+
.sort((a, b) => (b.added || 0) - (a.added || 0));
|
|
1070
|
+
};
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
### Recommendation Engine
|
|
1074
|
+
|
|
1075
|
+
```typescript
|
|
1076
|
+
const getRecommendedGroups = async (userId: string) => {
|
|
1077
|
+
// Get user's current groups
|
|
1078
|
+
const userGroups = await connectionActions.getConnections(
|
|
1079
|
+
COLLECTIONS.USERS,
|
|
1080
|
+
userId,
|
|
1081
|
+
COLLECTIONS.GROUPS,
|
|
1082
|
+
{connectionType: CONNECTION_TYPES.MEMBER}
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
const userGroupIds = new Set(userGroups.map(g => g.toId));
|
|
1086
|
+
|
|
1087
|
+
// Get tags from user's groups
|
|
1088
|
+
const groupTags = await Promise.all(
|
|
1089
|
+
Array.from(userGroupIds).map(groupId =>
|
|
1090
|
+
tagActions.tagsByItem(groupId, COLLECTIONS.GROUPS)
|
|
1091
|
+
)
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
const allTags = groupTags.flat().map(t => t.name);
|
|
1095
|
+
const uniqueTags = [...new Set(allTags)];
|
|
1096
|
+
|
|
1097
|
+
// Find groups with similar tags
|
|
1098
|
+
const similarGroups = await Promise.all(
|
|
1099
|
+
uniqueTags.map(tag =>
|
|
1100
|
+
tagActions.itemsByTag(tag, COLLECTIONS.GROUPS)
|
|
1101
|
+
)
|
|
1102
|
+
);
|
|
1103
|
+
|
|
1104
|
+
// Filter out groups user is already in
|
|
1105
|
+
return similarGroups
|
|
1106
|
+
.flat()
|
|
1107
|
+
.filter(group => !userGroupIds.has(group.groupId))
|
|
1108
|
+
.slice(0, 10);
|
|
1109
|
+
};
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
## Best Practices
|
|
1113
|
+
|
|
1114
|
+
### 1. Use Appropriate Connection Types
|
|
1115
|
+
|
|
1116
|
+
```typescript
|
|
1117
|
+
// ✅ Correct - semantic connection types
|
|
1118
|
+
await connectionActions.addConnection(
|
|
1119
|
+
COLLECTIONS.USERS,
|
|
1120
|
+
userId,
|
|
1121
|
+
COLLECTIONS.GROUPS,
|
|
1122
|
+
groupId,
|
|
1123
|
+
CONNECTION_TYPES.ADMIN // Clear meaning
|
|
1124
|
+
);
|
|
1125
|
+
|
|
1126
|
+
// ❌ Incorrect - generic connection type
|
|
1127
|
+
await connectionActions.addConnection(
|
|
1128
|
+
COLLECTIONS.USERS,
|
|
1129
|
+
userId,
|
|
1130
|
+
COLLECTIONS.GROUPS,
|
|
1131
|
+
groupId,
|
|
1132
|
+
CONNECTION_TYPES.MEMBER // Too generic for admin
|
|
1133
|
+
);
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
### 2. Store Relevant Metadata
|
|
1137
|
+
|
|
1138
|
+
```typescript
|
|
1139
|
+
// ✅ Good - meaningful metadata
|
|
1140
|
+
await connectionActions.addConnection(
|
|
1141
|
+
COLLECTIONS.USERS,
|
|
1142
|
+
userId,
|
|
1143
|
+
COLLECTIONS.GROUPS,
|
|
1144
|
+
groupId,
|
|
1145
|
+
CONNECTION_TYPES.MEMBER,
|
|
1146
|
+
{
|
|
1147
|
+
joinedAt: Date.now(),
|
|
1148
|
+
invitedBy: inviterUserId,
|
|
1149
|
+
role: 'contributor',
|
|
1150
|
+
permissions: ['post', 'comment']
|
|
1151
|
+
}
|
|
1152
|
+
);
|
|
1153
|
+
|
|
1154
|
+
// ❌ Bad - missing context
|
|
1155
|
+
await connectionActions.addConnection(
|
|
1156
|
+
COLLECTIONS.USERS,
|
|
1157
|
+
userId,
|
|
1158
|
+
COLLECTIONS.GROUPS,
|
|
1159
|
+
groupId,
|
|
1160
|
+
CONNECTION_TYPES.MEMBER
|
|
1161
|
+
);
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
### 3. Clean Up Orphaned Connections
|
|
1165
|
+
|
|
1166
|
+
```typescript
|
|
1167
|
+
// When deleting a post, remove all connections
|
|
1168
|
+
const deletePostWithConnections = async (postId: string) => {
|
|
1169
|
+
// Remove reactions
|
|
1170
|
+
// Remove tags
|
|
1171
|
+
// Remove file connections
|
|
1172
|
+
// Remove conversation links
|
|
1173
|
+
|
|
1174
|
+
// Finally delete the post
|
|
1175
|
+
await postActions.delete(postId);
|
|
1176
|
+
};
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
### 4. Implement Bidirectional Checks
|
|
1180
|
+
|
|
1181
|
+
```typescript
|
|
1182
|
+
// Check both directions for symmetric relationships
|
|
1183
|
+
const areUsersFriends = async (userId1: string, userId2: string) => {
|
|
1184
|
+
const user1Following = await connectionActions.getConnections(
|
|
1185
|
+
COLLECTIONS.USERS,
|
|
1186
|
+
userId1,
|
|
1187
|
+
COLLECTIONS.USERS,
|
|
1188
|
+
{connectionType: CONNECTION_TYPES.FOLLOWING}
|
|
1189
|
+
);
|
|
1190
|
+
|
|
1191
|
+
const user2Following = await connectionActions.getConnections(
|
|
1192
|
+
COLLECTIONS.USERS,
|
|
1193
|
+
userId2,
|
|
1194
|
+
COLLECTIONS.USERS,
|
|
1195
|
+
{connectionType: CONNECTION_TYPES.FOLLOWING}
|
|
1196
|
+
);
|
|
1197
|
+
|
|
1198
|
+
const user1FollowsUser2 = user1Following.some(c => c.toId === userId2);
|
|
1199
|
+
const user2FollowsUser1 = user2Following.some(c => c.toId === userId1);
|
|
1200
|
+
|
|
1201
|
+
return user1FollowsUser2 && user2FollowsUser1;
|
|
1202
|
+
};
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
### 5. Batch Connection Operations
|
|
1206
|
+
|
|
1207
|
+
```typescript
|
|
1208
|
+
// ✅ Efficient - batch operations
|
|
1209
|
+
const addUserToGroups = async (userId: string, groupIds: string[]) => {
|
|
1210
|
+
await Promise.all(
|
|
1211
|
+
groupIds.map(groupId =>
|
|
1212
|
+
connectionActions.addConnection(
|
|
1213
|
+
COLLECTIONS.USERS,
|
|
1214
|
+
userId,
|
|
1215
|
+
COLLECTIONS.GROUPS,
|
|
1216
|
+
groupId,
|
|
1217
|
+
CONNECTION_TYPES.MEMBER
|
|
1218
|
+
)
|
|
1219
|
+
)
|
|
1220
|
+
);
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
// ❌ Inefficient - sequential operations
|
|
1224
|
+
const addUserToGroupsSequential = async (userId: string, groupIds: string[]) => {
|
|
1225
|
+
for (const groupId of groupIds) {
|
|
1226
|
+
await connectionActions.addConnection(
|
|
1227
|
+
COLLECTIONS.USERS,
|
|
1228
|
+
userId,
|
|
1229
|
+
COLLECTIONS.GROUPS,
|
|
1230
|
+
groupId,
|
|
1231
|
+
CONNECTION_TYPES.MEMBER
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
### 6. Cache Connection Queries
|
|
1238
|
+
|
|
1239
|
+
```typescript
|
|
1240
|
+
// Cache frequently accessed connections
|
|
1241
|
+
const connectionCache = new Map<string, ConnectionEdge[]>();
|
|
1242
|
+
|
|
1243
|
+
const getCachedConnections = async (
|
|
1244
|
+
fromType: CollectionType,
|
|
1245
|
+
fromId: string,
|
|
1246
|
+
toType?: CollectionType
|
|
1247
|
+
): Promise<ConnectionEdge[]> => {
|
|
1248
|
+
const cacheKey = `${fromType}:${fromId}:${toType || 'all'}`;
|
|
1249
|
+
|
|
1250
|
+
if (connectionCache.has(cacheKey)) {
|
|
1251
|
+
return connectionCache.get(cacheKey)!;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const connections = await connectionActions.getConnections(
|
|
1255
|
+
fromType,
|
|
1256
|
+
fromId,
|
|
1257
|
+
toType
|
|
1258
|
+
);
|
|
1259
|
+
|
|
1260
|
+
connectionCache.set(cacheKey, connections);
|
|
1261
|
+
|
|
1262
|
+
// Clear cache after 5 minutes
|
|
1263
|
+
setTimeout(() => connectionCache.delete(cacheKey), 300000);
|
|
1264
|
+
|
|
1265
|
+
return connections;
|
|
1266
|
+
};
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
### 7. Validate Before Creating Connections
|
|
1270
|
+
|
|
1271
|
+
```typescript
|
|
1272
|
+
const addMemberToGroup = async (userId: string, groupId: string) => {
|
|
1273
|
+
// Check if already member
|
|
1274
|
+
const existing = await connectionActions.getConnections(
|
|
1275
|
+
COLLECTIONS.USERS,
|
|
1276
|
+
userId,
|
|
1277
|
+
COLLECTIONS.GROUPS,
|
|
1278
|
+
{connectionType: CONNECTION_TYPES.MEMBER}
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1281
|
+
if (existing.some(conn => conn.toId === groupId)) {
|
|
1282
|
+
throw new Error('User is already a member');
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// Check if blocked
|
|
1286
|
+
const blocked = await connectionActions.getConnections(
|
|
1287
|
+
COLLECTIONS.USERS,
|
|
1288
|
+
userId,
|
|
1289
|
+
COLLECTIONS.GROUPS,
|
|
1290
|
+
{connectionType: CONNECTION_TYPES.BLOCKED}
|
|
1291
|
+
);
|
|
1292
|
+
|
|
1293
|
+
if (blocked.some(conn => conn.toId === groupId)) {
|
|
1294
|
+
throw new Error('User is blocked from this group');
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Add connection
|
|
1298
|
+
return await connectionActions.addConnection(
|
|
1299
|
+
COLLECTIONS.USERS,
|
|
1300
|
+
userId,
|
|
1301
|
+
COLLECTIONS.GROUPS,
|
|
1302
|
+
groupId,
|
|
1303
|
+
CONNECTION_TYPES.MEMBER
|
|
1304
|
+
);
|
|
1305
|
+
};
|
|
1306
|
+
```
|
|
1307
|
+
|
|
1308
|
+
### 8. Use Transactions for Complex Operations
|
|
1309
|
+
|
|
1310
|
+
```typescript
|
|
1311
|
+
// Atomic operation to transfer group ownership
|
|
1312
|
+
const transferGroupOwnership = async (
|
|
1313
|
+
groupId: string,
|
|
1314
|
+
currentOwnerId: string,
|
|
1315
|
+
newOwnerId: string
|
|
1316
|
+
) => {
|
|
1317
|
+
try {
|
|
1318
|
+
// Remove current owner's admin status
|
|
1319
|
+
await connectionActions.removeConnection(
|
|
1320
|
+
COLLECTIONS.USERS,
|
|
1321
|
+
currentOwnerId,
|
|
1322
|
+
COLLECTIONS.GROUPS,
|
|
1323
|
+
groupId
|
|
1324
|
+
);
|
|
1325
|
+
|
|
1326
|
+
// Add current owner as regular member
|
|
1327
|
+
await connectionActions.addConnection(
|
|
1328
|
+
COLLECTIONS.USERS,
|
|
1329
|
+
currentOwnerId,
|
|
1330
|
+
COLLECTIONS.GROUPS,
|
|
1331
|
+
groupId,
|
|
1332
|
+
CONNECTION_TYPES.MEMBER
|
|
1333
|
+
);
|
|
1334
|
+
|
|
1335
|
+
// Remove new owner's member status
|
|
1336
|
+
await connectionActions.removeConnection(
|
|
1337
|
+
COLLECTIONS.USERS,
|
|
1338
|
+
newOwnerId,
|
|
1339
|
+
COLLECTIONS.GROUPS,
|
|
1340
|
+
groupId
|
|
1341
|
+
);
|
|
1342
|
+
|
|
1343
|
+
// Add new owner as admin
|
|
1344
|
+
await connectionActions.addConnection(
|
|
1345
|
+
COLLECTIONS.USERS,
|
|
1346
|
+
newOwnerId,
|
|
1347
|
+
COLLECTIONS.GROUPS,
|
|
1348
|
+
groupId,
|
|
1349
|
+
CONNECTION_TYPES.ADMIN,
|
|
1350
|
+
{
|
|
1351
|
+
grantedAt: Date.now(),
|
|
1352
|
+
grantedBy: currentOwnerId
|
|
1353
|
+
}
|
|
1354
|
+
);
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
// Rollback logic if needed
|
|
1357
|
+
console.error('Transfer failed:', error);
|
|
1358
|
+
throw error;
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
```
|
|
1362
|
+
|
|
1363
|
+
## Next Steps
|
|
1364
|
+
|
|
1365
|
+
- [CRUD Integration Guide](./CRUD_INTEGRATION.md) - Learn CRUD patterns
|
|
1366
|
+
- [Collections Reference](./COLLECTIONS.md) - Collection field details
|
|
1367
|
+
- [MetropolisJS Documentation](../README.md) - Main library docs
|