@rocapine/community-core 0.1.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.
@@ -0,0 +1,7 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ import { type CommunityConfig, type ResolvedCommunityConfig } from "./config";
3
+ export declare function CommunityProvider(props: {
4
+ config: CommunityConfig;
5
+ children: ReactNode;
6
+ }): ReactElement;
7
+ export declare function useCommunityConfig(): ResolvedCommunityConfig;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CommunityProvider = CommunityProvider;
4
+ exports.useCommunityConfig = useCommunityConfig;
5
+ const react_1 = require("react");
6
+ const config_1 = require("./config");
7
+ const schema_check_1 = require("./schema-check");
8
+ const CommunityConfigContext = (0, react_1.createContext)(null);
9
+ function CommunityProvider(props) {
10
+ const { config, children } = props;
11
+ const resolved = (0, react_1.useMemo)(() => (0, config_1.resolveConfig)(config), [config]);
12
+ (0, react_1.useEffect)(() => {
13
+ void (0, schema_check_1.checkSchemaVersion)(resolved);
14
+ }, [resolved]);
15
+ return (<CommunityConfigContext.Provider value={resolved}>{children}</CommunityConfigContext.Provider>);
16
+ }
17
+ function useCommunityConfig() {
18
+ const resolved = (0, react_1.useContext)(CommunityConfigContext);
19
+ if (!resolved) {
20
+ throw new Error("CommunityProvider is missing");
21
+ }
22
+ return resolved;
23
+ }
@@ -0,0 +1,19 @@
1
+ import type { ResolvedCommunityConfig } from "./config";
2
+ /** Bump whenever the SDK's expected `community_meta` shape changes. */
3
+ export declare const REQUIRED_SCHEMA_VERSION = 1;
4
+ /**
5
+ * Read path, exported separately from the `__DEV__` gate so tests — where
6
+ * `__DEV__` is `undefined` under vitest/Node, not `false` — can exercise the
7
+ * four behaviors (mismatch warns, match is silent, query error is silent,
8
+ * degraded mode is silent) directly. `checkSchemaVersion` below is the only
9
+ * symbol wired into `CommunityProvider`; this one stays package-internal
10
+ * (not re-exported from `index.ts`).
11
+ */
12
+ export declare function checkSchemaVersionUnguarded(cfg: ResolvedCommunityConfig): Promise<void>;
13
+ /**
14
+ * Dev-only entry point, called once from `CommunityProvider`'s mount effect.
15
+ * No-ops outside dev (production RN bundles define `__DEV__ === false`; a
16
+ * plain Node/test environment leaves it `undefined`, which also no-ops here —
17
+ * see `checkSchemaVersionUnguarded` for how tests reach the read path).
18
+ */
19
+ export declare function checkSchemaVersion(cfg: ResolvedCommunityConfig): Promise<void>;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ // Dev-only schema drift check. Reads the single `community_meta.schema_version`
3
+ // row and warns when it doesn't match what this SDK version expects, so a host
4
+ // developer notices a stale backend migration during development. This is a
5
+ // diagnostic aid, not a runtime gate — it must never throw and must never run
6
+ // in production (RN's global `__DEV__` gate, same convention every host app
7
+ // already relies on for dev-only logging).
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.REQUIRED_SCHEMA_VERSION = void 0;
10
+ exports.checkSchemaVersionUnguarded = checkSchemaVersionUnguarded;
11
+ exports.checkSchemaVersion = checkSchemaVersion;
12
+ /** Bump whenever the SDK's expected `community_meta` shape changes. */
13
+ exports.REQUIRED_SCHEMA_VERSION = 1;
14
+ /**
15
+ * Read path, exported separately from the `__DEV__` gate so tests — where
16
+ * `__DEV__` is `undefined` under vitest/Node, not `false` — can exercise the
17
+ * four behaviors (mismatch warns, match is silent, query error is silent,
18
+ * degraded mode is silent) directly. `checkSchemaVersion` below is the only
19
+ * symbol wired into `CommunityProvider`; this one stays package-internal
20
+ * (not re-exported from `index.ts`).
21
+ */
22
+ async function checkSchemaVersionUnguarded(cfg) {
23
+ const client = cfg.supabase;
24
+ if (!client)
25
+ return; // degraded mode (no backend configured) — nothing to check
26
+ try {
27
+ const { data, error } = await client
28
+ .from("community_meta")
29
+ .select("schema_version")
30
+ .maybeSingle();
31
+ if (error)
32
+ return; // e.g. table doesn't exist yet on this backend — silent
33
+ const version = data?.schema_version;
34
+ if (version !== undefined && version !== exports.REQUIRED_SCHEMA_VERSION) {
35
+ console.warn(`[@rocapine/community-core] community_meta.schema_version is ${version}, this SDK expects ${exports.REQUIRED_SCHEMA_VERSION}. Run the latest backend migrations.`);
36
+ }
37
+ }
38
+ catch {
39
+ // network error or unexpected client shape — never throw from a dev diagnostic
40
+ }
41
+ }
42
+ /**
43
+ * Dev-only entry point, called once from `CommunityProvider`'s mount effect.
44
+ * No-ops outside dev (production RN bundles define `__DEV__ === false`; a
45
+ * plain Node/test environment leaves it `undefined`, which also no-ops here —
46
+ * see `checkSchemaVersionUnguarded` for how tests reach the read path).
47
+ */
48
+ async function checkSchemaVersion(cfg) {
49
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
50
+ await checkSchemaVersionUnguarded(cfg);
51
+ }
52
+ }
@@ -0,0 +1,147 @@
1
+ import type { ResolvedCommunityConfig } from "./config";
2
+ import { type CommunityProfile, type FeedPost, type ReactionData, type ThreadComment } from "./models";
3
+ /**
4
+ * Appends `extraPostColumns` (if any) to the posts feed select. Pure and
5
+ * exported so it's unit-testable without a Supabase client. Invalid column
6
+ * names are dropped with a `console.warn` rather than failing the query.
7
+ */
8
+ export declare function buildFeedSelect(extraPostColumns?: readonly string[]): string;
9
+ /**
10
+ * Batched reaction state for one page: count + last reactor's display name + my-reaction
11
+ * flag, all from the single security-definer `post_reaction_summary` RPC (the backend
12
+ * already resolves "did I react" server-side, so unlike the mold's prayer batching this
13
+ * needs no second query). Gated on `cfg.modules.reaction`; a missing/erroring RPC (e.g.
14
+ * the reaction module's migration not installed yet) degrades to no reaction data instead
15
+ * of taking the whole feed down.
16
+ */
17
+ export declare function fetchReactionSummaries(cfg: ResolvedCommunityConfig, postIds: string[]): Promise<ReactionData>;
18
+ /**
19
+ * One feed page, newest first. RLS already excludes other users' hidden and
20
+ * blocked-author content; we only exclude our own soft-deleted posts.
21
+ */
22
+ export declare function fetchFeedPage(cfg: ResolvedCommunityConfig, opts?: {
23
+ topic?: string;
24
+ cursor?: number;
25
+ }): Promise<FeedPost[]>;
26
+ /**
27
+ * Count of visible posts newer than `sinceIso`, excluding my own, for the
28
+ * given topic. RLS already excludes blocked authors and hidden/deleted rows.
29
+ * `topic` is beyond the brief's literal signature but kept (both source apps
30
+ * take it) so a topic-filtered feed's "N new posts" pill stays accurate.
31
+ */
32
+ export declare function countNewPosts(cfg: ResolvedCommunityConfig, sinceIso: string, topic?: string): Promise<number>;
33
+ /** A single profile by id, e.g. for the profile page header. */
34
+ export declare function fetchProfile(cfg: ResolvedCommunityConfig, userId: string): Promise<CommunityProfile>;
35
+ /**
36
+ * One user's posts, newest first (no pinned ordering — a profile page is
37
+ * strictly chronological). Same visibility rules as the main feed, same
38
+ * page-index cursor as `fetchFeedPage` (product decision, reversing the
39
+ * earlier single-page scope cut).
40
+ */
41
+ export declare function fetchUserPosts(cfg: ResolvedCommunityConfig, userId: string, opts?: {
42
+ cursor?: number;
43
+ }): Promise<FeedPost[]>;
44
+ /**
45
+ * Full-text-ish search over visible post content (newest first), same
46
+ * page-index cursor as `fetchFeedPage` (product decision, reversing the
47
+ * earlier single-page scope cut).
48
+ */
49
+ export declare function searchPosts(cfg: ResolvedCommunityConfig, query: string, opts?: {
50
+ cursor?: number;
51
+ }): Promise<FeedPost[]>;
52
+ /**
53
+ * Note: `cfg.feed.extraPostColumns`/`transformPost` do NOT apply here — this
54
+ * queries `comments`, not `posts` (the thread's parent post is passed in by
55
+ * the caller from an already-fetched, already-transformed `FeedPost`, not
56
+ * re-fetched).
57
+ */
58
+ export declare function fetchThread(cfg: ResolvedCommunityConfig, postId: string): Promise<ThreadComment[]>;
59
+ /** Verdict returned by the synchronous moderate-one Edge Function. */
60
+ export type ModerationVerdict = {
61
+ status: "published" | "rejected" | "pending";
62
+ reason?: string;
63
+ };
64
+ export type CreatePostInput = {
65
+ topic: string;
66
+ body: string;
67
+ pollOptions?: string[];
68
+ };
69
+ /**
70
+ * Insert a post as 'pending' (invisible to others until moderated). Returns id.
71
+ * With pollOptions, the post and its options are created transactionally by
72
+ * the create_poll_post RPC (security invoker, same RLS as a plain insert);
73
+ * that branch is gated on `cfg.modules.polls`.
74
+ */
75
+ export declare function createPost(cfg: ResolvedCommunityConfig, input: CreatePostInput): Promise<string>;
76
+ /**
77
+ * Cast or change my vote: one row per (post, user), upsert swaps the option.
78
+ * `optionId` is the poll option's row id (matching `FeedPoll.myOptionId` /
79
+ * `applyPollVote`'s optimistic-update contract from Task 3), not a numeric
80
+ * index — see the task report for this naming call against the brief.
81
+ * Gated on `cfg.modules.polls` for consistent DX with the reaction module.
82
+ */
83
+ export declare function votePoll(cfg: ResolvedCommunityConfig, postId: string, optionId: string): Promise<void>;
84
+ /** Insert a comment as 'pending'. Returns id. */
85
+ export declare function createComment(cfg: ResolvedCommunityConfig, postId: string, body: string): Promise<string>;
86
+ /** Run synchronous moderation on a just-created pending post/comment. */
87
+ export declare function moderateOne(cfg: ResolvedCommunityConfig, input: {
88
+ kind: "post" | "comment";
89
+ id: string;
90
+ }): Promise<ModerationVerdict>;
91
+ export declare function setLike(cfg: ResolvedCommunityConfig, postId: string, on: boolean): Promise<void>;
92
+ /**
93
+ * `setPrayed` generalized to `setReaction`: insert my private, non-retractable
94
+ * reaction row (rows are never deleted — see spec §6). Gated on `cfg.modules.reaction`.
95
+ */
96
+ export declare function setReaction(cfg: ResolvedCommunityConfig, postId: string): Promise<void>;
97
+ export type ReportReason = "spam" | "harassment" | "hate" | "inappropriate" | "other";
98
+ export interface ReportInput {
99
+ reportedUserId: string;
100
+ postId?: string;
101
+ commentId?: string;
102
+ reason: ReportReason;
103
+ details?: string;
104
+ }
105
+ export declare function reportContent(cfg: ResolvedCommunityConfig, input: ReportInput): Promise<void>;
106
+ export declare function blockUser(cfg: ResolvedCommunityConfig, blockedId: string): Promise<void>;
107
+ export declare function deleteOwnPost(cfg: ResolvedCommunityConfig, postId: string): Promise<void>;
108
+ export declare function deleteOwnComment(cfg: ResolvedCommunityConfig, commentId: string): Promise<void>;
109
+ /**
110
+ * `avatarUrl` here is actually the Storage *path* returned by `uploadAvatar`
111
+ * (mold field name `avatarPath`) — kept as `avatarUrl` per the brief's produced
112
+ * signature and translated to the Edge Function's expected `avatarPath` body
113
+ * key at the call site below.
114
+ */
115
+ export type UpdateProfileInput = {
116
+ handle?: string;
117
+ bio?: string;
118
+ avatarUrl?: string;
119
+ };
120
+ export type UpdateProfileResult = {
121
+ status: "ok";
122
+ handle: string | null;
123
+ bio: string | null;
124
+ avatarUrl: string | null;
125
+ } | {
126
+ status: "rejected";
127
+ field: "handle" | "bio" | "avatar";
128
+ reason?: string;
129
+ } | {
130
+ status: "error";
131
+ code?: "handle_taken";
132
+ error?: string;
133
+ };
134
+ /**
135
+ * The only write path for handle/bio/avatar — moderated server-side by the
136
+ * `update-profile` Edge Function. `invoke` treats any non-2xx as `error`; a
137
+ * `FunctionsHttpError` carries the typed JSON body (e.g. `handle_taken`) on
138
+ * its `context` response, other error kinds mean the request never landed.
139
+ */
140
+ export declare function updateProfile(cfg: ResolvedCommunityConfig, input: UpdateProfileInput): Promise<UpdateProfileResult>;
141
+ /**
142
+ * Upload a JPEG avatar to the `avatars` bucket from a local file URI. Returns
143
+ * the storage path. Takes a `fileUri` (not a base64 string, unlike the mold)
144
+ * so the core has no Hermes/`atob` dependency — `fetch` on a `file://` URI is
145
+ * the portable way to read local bytes in React Native.
146
+ */
147
+ export declare function uploadAvatar(cfg: ResolvedCommunityConfig, fileUri: string): Promise<string>;