@stapel/reviews-react 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.
Files changed (164) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/MODULE.md +151 -0
  3. package/README.md +207 -0
  4. package/dist/api/generated/schema.d.ts +360 -0
  5. package/dist/api/generated/schema.d.ts.map +1 -0
  6. package/dist/api/generated/schema.js +2 -0
  7. package/dist/api/generated/schema.js.map +1 -0
  8. package/dist/api/reviewsApi.d.ts +89 -0
  9. package/dist/api/reviewsApi.d.ts.map +1 -0
  10. package/dist/api/reviewsApi.js +51 -0
  11. package/dist/api/reviewsApi.js.map +1 -0
  12. package/dist/api/types.d.ts +127 -0
  13. package/dist/api/types.d.ts.map +1 -0
  14. package/dist/api/types.js +2 -0
  15. package/dist/api/types.js.map +1 -0
  16. package/dist/default/ErrorAlert.d.ts +9 -0
  17. package/dist/default/ErrorAlert.d.ts.map +1 -0
  18. package/dist/default/ErrorAlert.js +27 -0
  19. package/dist/default/ErrorAlert.js.map +1 -0
  20. package/dist/default/RatingBadge.d.ts +17 -0
  21. package/dist/default/RatingBadge.d.ts.map +1 -0
  22. package/dist/default/RatingBadge.js +23 -0
  23. package/dist/default/RatingBadge.js.map +1 -0
  24. package/dist/default/ReviewFormCard.d.ts +25 -0
  25. package/dist/default/ReviewFormCard.d.ts.map +1 -0
  26. package/dist/default/ReviewFormCard.js +42 -0
  27. package/dist/default/ReviewFormCard.js.map +1 -0
  28. package/dist/default/ReviewListPanel.d.ts +29 -0
  29. package/dist/default/ReviewListPanel.d.ts.map +1 -0
  30. package/dist/default/ReviewListPanel.js +47 -0
  31. package/dist/default/ReviewListPanel.js.map +1 -0
  32. package/dist/default/ReviewsPanel.d.ts +33 -0
  33. package/dist/default/ReviewsPanel.d.ts.map +1 -0
  34. package/dist/default/ReviewsPanel.js +17 -0
  35. package/dist/default/ReviewsPanel.js.map +1 -0
  36. package/dist/default/index.d.ts +31 -0
  37. package/dist/default/index.d.ts.map +1 -0
  38. package/dist/default/index.js +25 -0
  39. package/dist/default/index.js.map +1 -0
  40. package/dist/default/theme.d.ts +24 -0
  41. package/dist/default/theme.d.ts.map +1 -0
  42. package/dist/default/theme.js +39 -0
  43. package/dist/default/theme.js.map +1 -0
  44. package/dist/default/types.d.ts +12 -0
  45. package/dist/default/types.d.ts.map +1 -0
  46. package/dist/default/types.js +2 -0
  47. package/dist/default/types.js.map +1 -0
  48. package/dist/headless/ReviewAggregate.d.ts +63 -0
  49. package/dist/headless/ReviewAggregate.d.ts.map +1 -0
  50. package/dist/headless/ReviewAggregate.js +55 -0
  51. package/dist/headless/ReviewAggregate.js.map +1 -0
  52. package/dist/headless/ReviewForm.d.ts +83 -0
  53. package/dist/headless/ReviewForm.d.ts.map +1 -0
  54. package/dist/headless/ReviewForm.js +75 -0
  55. package/dist/headless/ReviewForm.js.map +1 -0
  56. package/dist/headless/ReviewList.d.ts +47 -0
  57. package/dist/headless/ReviewList.d.ts.map +1 -0
  58. package/dist/headless/ReviewList.js +51 -0
  59. package/dist/headless/ReviewList.js.map +1 -0
  60. package/dist/headless/ReviewsProvider.d.ts +19 -0
  61. package/dist/headless/ReviewsProvider.d.ts.map +1 -0
  62. package/dist/headless/ReviewsProvider.js +15 -0
  63. package/dist/headless/ReviewsProvider.js.map +1 -0
  64. package/dist/i18n/errorsMap.d.ts +12 -0
  65. package/dist/i18n/errorsMap.d.ts.map +1 -0
  66. package/dist/i18n/errorsMap.js +22 -0
  67. package/dist/i18n/errorsMap.js.map +1 -0
  68. package/dist/i18n/es.d.ts +19 -0
  69. package/dist/i18n/es.d.ts.map +1 -0
  70. package/dist/i18n/es.js +66 -0
  71. package/dist/i18n/es.js.map +1 -0
  72. package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
  73. package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
  74. package/dist/i18n/generated/errors.es.gen.js +58 -0
  75. package/dist/i18n/generated/errors.es.gen.js.map +1 -0
  76. package/dist/i18n/generated/errors.gen.d.ts +341 -0
  77. package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
  78. package/dist/i18n/generated/errors.gen.js +174 -0
  79. package/dist/i18n/generated/errors.gen.js.map +1 -0
  80. package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
  81. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
  82. package/dist/i18n/generated/errors.ru.gen.js +58 -0
  83. package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
  84. package/dist/i18n/keys.d.ts +59 -0
  85. package/dist/i18n/keys.d.ts.map +1 -0
  86. package/dist/i18n/keys.js +115 -0
  87. package/dist/i18n/keys.js.map +1 -0
  88. package/dist/i18n/ru.d.ts +22 -0
  89. package/dist/i18n/ru.d.ts.map +1 -0
  90. package/dist/i18n/ru.js +70 -0
  91. package/dist/i18n/ru.js.map +1 -0
  92. package/dist/index.d.ts +98 -0
  93. package/dist/index.d.ts.map +1 -0
  94. package/dist/index.js +92 -0
  95. package/dist/index.js.map +1 -0
  96. package/dist/model/context.d.ts +11 -0
  97. package/dist/model/context.d.ts.map +1 -0
  98. package/dist/model/context.js +16 -0
  99. package/dist/model/context.js.map +1 -0
  100. package/dist/model/list.d.ts +52 -0
  101. package/dist/model/list.d.ts.map +1 -0
  102. package/dist/model/list.js +60 -0
  103. package/dist/model/list.js.map +1 -0
  104. package/dist/model/mutations.d.ts +32 -0
  105. package/dist/model/mutations.d.ts.map +1 -0
  106. package/dist/model/mutations.js +44 -0
  107. package/dist/model/mutations.js.map +1 -0
  108. package/dist/model/queries.d.ts +65 -0
  109. package/dist/model/queries.d.ts.map +1 -0
  110. package/dist/model/queries.js +80 -0
  111. package/dist/model/queries.js.map +1 -0
  112. package/dist/model/queryKeys.d.ts +27 -0
  113. package/dist/model/queryKeys.d.ts.map +1 -0
  114. package/dist/model/queryKeys.js +14 -0
  115. package/dist/model/queryKeys.js.map +1 -0
  116. package/dist/model/rating.d.ts +57 -0
  117. package/dist/model/rating.d.ts.map +1 -0
  118. package/dist/model/rating.js +47 -0
  119. package/dist/model/rating.js.map +1 -0
  120. package/dist/model/refusals.d.ts +51 -0
  121. package/dist/model/refusals.d.ts.map +1 -0
  122. package/dist/model/refusals.js +103 -0
  123. package/dist/model/refusals.js.map +1 -0
  124. package/dist/model/runtime.d.ts +44 -0
  125. package/dist/model/runtime.d.ts.map +1 -0
  126. package/dist/model/runtime.js +15 -0
  127. package/dist/model/runtime.js.map +1 -0
  128. package/llms.txt +80 -0
  129. package/manifest.json +577 -0
  130. package/package.json +115 -0
  131. package/src/analytics/generated/events.json +7 -0
  132. package/src/api/generated/schema.ts +362 -0
  133. package/src/api/reviewsApi.ts +170 -0
  134. package/src/api/types.ts +140 -0
  135. package/src/default/ErrorAlert.tsx +46 -0
  136. package/src/default/RatingBadge.tsx +81 -0
  137. package/src/default/ReviewFormCard.tsx +139 -0
  138. package/src/default/ReviewListPanel.tsx +204 -0
  139. package/src/default/ReviewsPanel.tsx +74 -0
  140. package/src/default/index.ts +30 -0
  141. package/src/default/theme.tsx +66 -0
  142. package/src/default/types.ts +12 -0
  143. package/src/headless/ReviewAggregate.tsx +94 -0
  144. package/src/headless/ReviewForm.tsx +163 -0
  145. package/src/headless/ReviewList.tsx +99 -0
  146. package/src/headless/ReviewsProvider.tsx +20 -0
  147. package/src/i18n/errorsMap.ts +33 -0
  148. package/src/i18n/es.ts +81 -0
  149. package/src/i18n/generated/errors.es.gen.ts +64 -0
  150. package/src/i18n/generated/errors.gen.ts +205 -0
  151. package/src/i18n/generated/errors.json +387 -0
  152. package/src/i18n/generated/errors.ru.gen.ts +64 -0
  153. package/src/i18n/keys.ts +133 -0
  154. package/src/i18n/ru.ts +85 -0
  155. package/src/index.ts +161 -0
  156. package/src/model/context.tsx +28 -0
  157. package/src/model/list.ts +74 -0
  158. package/src/model/mutations.ts +62 -0
  159. package/src/model/queries.ts +122 -0
  160. package/src/model/queryKeys.ts +45 -0
  161. package/src/model/rating.ts +101 -0
  162. package/src/model/refusals.ts +114 -0
  163. package/src/model/runtime.ts +61 -0
  164. package/tsconfig.json +26 -0
@@ -0,0 +1,362 @@
1
+ // AUTO-GENERATED by scripts/gen-api.mjs — do not edit by hand.
2
+ // Source: the backend module's own docs/schema.json (§17-native per-module contract).
3
+ // Regenerate: pnpm gen:api · Drift gate: pnpm gen:api:check
4
+ export interface paths {
5
+ "/reviews/api/v1/reviews": {
6
+ parameters: {
7
+ query?: never;
8
+ header?: never;
9
+ path?: never;
10
+ cookie?: never;
11
+ };
12
+ /**
13
+ * @description List a target's reviews (anchor-paginated, published-only for
14
+ * non-moderators), or create a review.
15
+ *
16
+ * ``GET`` is anonymously readable (storefront F5 verdict,
17
+ * the storefront spec §13.8 note 2) — published-only filtering
18
+ * already guarantees a guest sees nothing a moderator would need to hide.
19
+ * ``POST`` still requires a real identity (there is an author to attribute
20
+ * the review to), so the class gate is DRF's own
21
+ * ``IsAuthenticatedOrReadOnly`` (mirrors ``ListingViewSet`` in
22
+ * stapel-listings — the fleet's other read-open/write-authenticated view in
23
+ * a single class) rather than a per-method override.
24
+ *
25
+ * **Permissions:** `IsAuthenticatedOrReadOnly`
26
+ */
27
+ get: operations["reviews_api_v1_reviews_retrieve"];
28
+ put?: never;
29
+ /**
30
+ * @description List a target's reviews (anchor-paginated, published-only for
31
+ * non-moderators), or create a review.
32
+ *
33
+ * ``GET`` is anonymously readable (storefront F5 verdict,
34
+ * the storefront spec §13.8 note 2) — published-only filtering
35
+ * already guarantees a guest sees nothing a moderator would need to hide.
36
+ * ``POST`` still requires a real identity (there is an author to attribute
37
+ * the review to), so the class gate is DRF's own
38
+ * ``IsAuthenticatedOrReadOnly`` (mirrors ``ListingViewSet`` in
39
+ * stapel-listings — the fleet's other read-open/write-authenticated view in
40
+ * a single class) rather than a per-method override.
41
+ *
42
+ * **Permissions:** `IsAuthenticatedOrReadOnly`
43
+ */
44
+ post: operations["reviews_api_v1_reviews_create"];
45
+ delete?: never;
46
+ options?: never;
47
+ head?: never;
48
+ patch?: never;
49
+ trace?: never;
50
+ };
51
+ "/reviews/api/v1/reviews/{review_id}/moderate": {
52
+ parameters: {
53
+ query?: never;
54
+ header?: never;
55
+ path?: never;
56
+ cookie?: never;
57
+ };
58
+ get?: never;
59
+ put?: never;
60
+ /**
61
+ * @description Hide or publish a review (moderation; owner/moderator-only via the
62
+ * target type's can_moderate callback).
63
+ *
64
+ * **Permissions:** `IsAuthenticated`
65
+ */
66
+ post: operations["reviews_api_v1_reviews_moderate_create"];
67
+ delete?: never;
68
+ options?: never;
69
+ head?: never;
70
+ patch?: never;
71
+ trace?: never;
72
+ };
73
+ "/reviews/api/v1/reviews/{review_id}/response": {
74
+ parameters: {
75
+ query?: never;
76
+ header?: never;
77
+ path?: never;
78
+ cookie?: never;
79
+ };
80
+ get?: never;
81
+ put?: never;
82
+ /**
83
+ * @description Attach the target owner's single reply to a review (owner-only via the
84
+ * target type's can_moderate callback; gated by the allow_response policy).
85
+ *
86
+ * **Permissions:** `IsAuthenticated`
87
+ */
88
+ post: operations["reviews_api_v1_reviews_response_create"];
89
+ delete?: never;
90
+ options?: never;
91
+ head?: never;
92
+ patch?: never;
93
+ trace?: never;
94
+ };
95
+ "/reviews/api/v1/reviews/aggregate": {
96
+ parameters: {
97
+ query?: never;
98
+ header?: never;
99
+ path?: never;
100
+ cookie?: never;
101
+ };
102
+ /**
103
+ * @description The module-owned rating aggregate (avg/count over published reviews)
104
+ * for a target.
105
+ *
106
+ * Anonymously readable for the same reason as the list's ``GET``
107
+ * (storefront F5 verdict, the storefront spec §13.8 note 2): the
108
+ * aggregate is computed over published reviews only, so a guest learns
109
+ * nothing a moderator would need withheld.
110
+ *
111
+ * **Permissions:** `AllowAny`
112
+ */
113
+ get: operations["reviews_api_v1_reviews_aggregate_retrieve"];
114
+ put?: never;
115
+ post?: never;
116
+ delete?: never;
117
+ options?: never;
118
+ head?: never;
119
+ patch?: never;
120
+ trace?: never;
121
+ };
122
+ }
123
+ export type webhooks = Record<string, never>;
124
+ export interface components {
125
+ schemas: {
126
+ /** @description Rating aggregate for a target over published reviews. */
127
+ AggregateResponse: {
128
+ /** @description The host-registered target-type key */
129
+ target_type: string;
130
+ /** @description Opaque host-owned target identifier */
131
+ target_key: string;
132
+ /**
133
+ * Format: double
134
+ * @description Mean rating (0.0 when count is 0)
135
+ */
136
+ avg: number;
137
+ /** @description Number of published reviews */
138
+ count: number;
139
+ };
140
+ /** @description Moderate a review. */
141
+ ModerateRequest: {
142
+ /** @description hide or publish */
143
+ action: string;
144
+ /** @description Optional moderation reason (carried in the emitted fact) */
145
+ reason?: string;
146
+ };
147
+ /** @description Attach the target owner's reply to a review. */
148
+ RespondRequest: {
149
+ /** @description Reply text */
150
+ body?: string;
151
+ };
152
+ /** @description A target owner's reply to a review. */
153
+ ResponseResponse: {
154
+ /** @description The responder's user id (the target owner) */
155
+ author_id: string;
156
+ /** @description Reply text */
157
+ body: string;
158
+ /**
159
+ * Format: date-time
160
+ * @description When the reply was written
161
+ */
162
+ created_at: string;
163
+ };
164
+ /** @description Create a review. */
165
+ ReviewCreateRequest: {
166
+ /** @description The host-registered target-type key */
167
+ target_type: string;
168
+ /** @description Opaque host-owned target identifier */
169
+ target_key: string;
170
+ /** @description Numeric rating (RATING_MIN..RATING_MAX) */
171
+ rating: number;
172
+ /** @description Optional review body */
173
+ body?: string;
174
+ };
175
+ /**
176
+ * @description The envelope ``GET /reviews`` actually returns — ``AnchorPagination``'s
177
+ * keys (``stapel_core.django.api.pagination.AnchorPagination.
178
+ * get_paginated_response``/``get_paginated_response_schema``) wrapping
179
+ * ``items``. Schema-only: ``ReviewListCreateView`` is a bare ``APIView``
180
+ * (views.py), so drf-spectacular's pagination auto-introspection — which
181
+ * only fires for ``GenericAPIView.pagination_class`` — never sees
182
+ * ``ReviewAnchorPagination``, and the envelope has to be declared by hand
183
+ * or spectacular renders the response as a bare array
184
+ * (the storefront spec §13.8 note 3).
185
+ */
186
+ ReviewPage: {
187
+ items: components["schemas"]["ReviewResponse"][];
188
+ next_anchor: string | null;
189
+ prev_anchor: string | null;
190
+ has_next: boolean;
191
+ has_prev: boolean;
192
+ count: number;
193
+ };
194
+ /** @description A review of an opaque target. */
195
+ ReviewResponse: {
196
+ /** @description Review id (UUID) */
197
+ id: string;
198
+ /** @description The host-registered target-type key */
199
+ target_type: string;
200
+ /** @description Opaque host-owned target identifier */
201
+ target_key: string;
202
+ /** @description Reviewer's user id */
203
+ author_id: string;
204
+ /** @description Numeric rating (RATING_MIN..RATING_MAX) */
205
+ rating: number;
206
+ /** @description Free-text review body */
207
+ body: string;
208
+ /** @description pending/published/hidden */
209
+ status: string;
210
+ /**
211
+ * Format: date-time
212
+ * @description Creation time
213
+ */
214
+ created_at: string;
215
+ /** @description The owner's reply, if any */
216
+ response?: components["schemas"]["ResponseResponse"] | null;
217
+ };
218
+ };
219
+ responses: never;
220
+ parameters: never;
221
+ requestBodies: never;
222
+ headers: never;
223
+ pathItems: never;
224
+ }
225
+ export type $defs = Record<string, never>;
226
+ export interface operations {
227
+ reviews_api_v1_reviews_retrieve: {
228
+ parameters: {
229
+ query: {
230
+ /** @description Opaque cursor: a `next_anchor`/`prev_anchor` from a previous page (the review's `created_at`). Omit for the first page. */
231
+ anchor?: string;
232
+ /** @description Which side of `anchor` to page toward. Default `next`. */
233
+ direction?: "center" | "next" | "prev";
234
+ /** @description Omit for published-only. `all` additionally asks for pending/hidden reviews, but is honored only for a moderator/owner of the target (the type's can_moderate callback) — a non-moderator asking for `all` is silently narrowed to published, never an error. */
235
+ include?: string;
236
+ /** @description Page size, default 20, max 100. */
237
+ limit?: number;
238
+ /** @description Opaque host-owned target identifier. */
239
+ target_key: string;
240
+ /** @description Host-registered target-type key (registry.py). */
241
+ target_type: string;
242
+ };
243
+ header?: never;
244
+ path?: never;
245
+ cookie?: never;
246
+ };
247
+ requestBody?: never;
248
+ responses: {
249
+ 200: {
250
+ headers: {
251
+ [name: string]: unknown;
252
+ };
253
+ content: {
254
+ "application/json": components["schemas"]["ReviewPage"];
255
+ };
256
+ };
257
+ };
258
+ };
259
+ reviews_api_v1_reviews_create: {
260
+ parameters: {
261
+ query?: never;
262
+ header?: never;
263
+ path?: never;
264
+ cookie?: never;
265
+ };
266
+ requestBody: {
267
+ content: {
268
+ "application/json": components["schemas"]["ReviewCreateRequest"];
269
+ "application/x-www-form-urlencoded": components["schemas"]["ReviewCreateRequest"];
270
+ "multipart/form-data": components["schemas"]["ReviewCreateRequest"];
271
+ };
272
+ };
273
+ responses: {
274
+ 201: {
275
+ headers: {
276
+ [name: string]: unknown;
277
+ };
278
+ content: {
279
+ "application/json": components["schemas"]["ReviewResponse"];
280
+ };
281
+ };
282
+ };
283
+ };
284
+ reviews_api_v1_reviews_moderate_create: {
285
+ parameters: {
286
+ query?: never;
287
+ header?: never;
288
+ path: {
289
+ review_id: string;
290
+ };
291
+ cookie?: never;
292
+ };
293
+ requestBody: {
294
+ content: {
295
+ "application/json": components["schemas"]["ModerateRequest"];
296
+ "application/x-www-form-urlencoded": components["schemas"]["ModerateRequest"];
297
+ "multipart/form-data": components["schemas"]["ModerateRequest"];
298
+ };
299
+ };
300
+ responses: {
301
+ 200: {
302
+ headers: {
303
+ [name: string]: unknown;
304
+ };
305
+ content: {
306
+ "application/json": components["schemas"]["ReviewResponse"];
307
+ };
308
+ };
309
+ };
310
+ };
311
+ reviews_api_v1_reviews_response_create: {
312
+ parameters: {
313
+ query?: never;
314
+ header?: never;
315
+ path: {
316
+ review_id: string;
317
+ };
318
+ cookie?: never;
319
+ };
320
+ requestBody?: {
321
+ content: {
322
+ "application/json": components["schemas"]["RespondRequest"];
323
+ "application/x-www-form-urlencoded": components["schemas"]["RespondRequest"];
324
+ "multipart/form-data": components["schemas"]["RespondRequest"];
325
+ };
326
+ };
327
+ responses: {
328
+ 201: {
329
+ headers: {
330
+ [name: string]: unknown;
331
+ };
332
+ content: {
333
+ "application/json": components["schemas"]["ReviewResponse"];
334
+ };
335
+ };
336
+ };
337
+ };
338
+ reviews_api_v1_reviews_aggregate_retrieve: {
339
+ parameters: {
340
+ query: {
341
+ /** @description Opaque host-owned target identifier. */
342
+ target_key: string;
343
+ /** @description Host-registered target-type key (registry.py). */
344
+ target_type: string;
345
+ };
346
+ header?: never;
347
+ path?: never;
348
+ cookie?: never;
349
+ };
350
+ requestBody?: never;
351
+ responses: {
352
+ 200: {
353
+ headers: {
354
+ [name: string]: unknown;
355
+ };
356
+ content: {
357
+ "application/json": components["schemas"]["AggregateResponse"];
358
+ };
359
+ };
360
+ };
361
+ };
362
+ }
@@ -0,0 +1,170 @@
1
+ import type { StapelClient, StapelRequestOptions } from "@stapel/core";
2
+ import type {
3
+ Review,
4
+ ReviewAggregate,
5
+ ReviewListParams,
6
+ ReviewPage,
7
+ ReviewTarget,
8
+ } from "./types.js";
9
+
10
+ /**
11
+ * CSRF rule for cookie-authenticated browser clients (mirrors auth-react and
12
+ * chat-react): the simplest SPA rule is to always send
13
+ * `X-Requested-With: XMLHttpRequest` on mutating requests. Header-token
14
+ * clients ignore it, so it is harmless there and every mutation carries it.
15
+ */
16
+ const CSRF_HEADERS: Record<string, string> = {
17
+ "X-Requested-With": "XMLHttpRequest",
18
+ };
19
+
20
+ function mutating(
21
+ options?: Omit<StapelRequestOptions, "method" | "body">
22
+ ): Omit<StapelRequestOptions, "method" | "body"> {
23
+ return { ...options, headers: { ...CSRF_HEADERS, ...options?.headers } };
24
+ }
25
+
26
+ /** The target pair, as the two endpoints that read it spell it. */
27
+ function targetQuery(target: ReviewTarget): Record<string, string> {
28
+ return { target_type: target.targetType, target_key: target.targetKey };
29
+ }
30
+
31
+ /**
32
+ * The pair's typed operation surface — one method per stapel-reviews endpoint
33
+ * a buyer's or a seller's browser may call, bound to the injected
34
+ * {@link StapelClient} (the per-module override seam of frontend-standard
35
+ * §7.2). Paths are relative to the runtime's `baseUrl` (`/reviews/api/v1`).
36
+ *
37
+ * ── The two reads are ANONYMOUS; the write is not ─────────────────────────
38
+ *
39
+ * Since stapel-reviews 0.3.0: `ReviewListCreateView` is
40
+ * `IsAuthenticatedOrReadOnly` (GET open, POST still needs a real identity —
41
+ * there is an author to attribute the review to) and `AggregateView` is
42
+ * `AllowAny`. Both declare `stapel_anonymous_access = ANONYMOUS_ALLOWED` and
43
+ * both are throttled from the module's own settings namespace
44
+ * (`LIST_THROTTLE` 120/min, `AGGREGATE_THROTTLE` 300/min), not the project's
45
+ * `DEFAULT_THROTTLE_RATES`.
46
+ *
47
+ * Nothing new became visible: both endpoints were already published-only for
48
+ * a non-moderator. What changed for this pair is that a guest on a public
49
+ * listing page now READS the reviews instead of being told to sign in — so
50
+ * `signInRequired` is gone from the two read bags and survives only on the
51
+ * write, where a 401 is still the honest answer.
52
+ *
53
+ * ── The two operations that are NOT here, and why ─────────────────────────
54
+ *
55
+ * `POST /reviews/{id}/moderate` (hide/publish) and `POST /reviews/{id}/response`
56
+ * (the target owner's single reply) are both gated on the target type's
57
+ * `can_moderate` callback, which is **fail-closed**: a type that names no
58
+ * callback denies everyone (`registry.check_can_moderate`). They belong to a
59
+ * seller console and a moderator console, and this pair ships neither
60
+ * (storefront spec §4.4: the owner's reply is DISPLAYED in the MVP and the
61
+ * button to write one does not exist, rather than existing switched off).
62
+ *
63
+ * Both stay in the generated schema and therefore in `manifest.json`, which
64
+ * lists the whole contract — nothing is hidden; they are simply not this
65
+ * pair's surface. Adding them later is additive and needs no change here.
66
+ *
67
+ * These operations will be GENERATED from schema.json operationIds by gen-api
68
+ * v2; until then they are hand-authored here (the ONE legal home of path
69
+ * strings — `stapel/no-string-paths` §2.3 carve-out).
70
+ */
71
+ export interface ReviewsApi {
72
+ readonly client: StapelClient;
73
+
74
+ /**
75
+ * A page of a target's reviews, newest first. Anonymous callers welcome.
76
+ *
77
+ * Answers core's `AnchorPagination` envelope — declared as
78
+ * `components/ReviewPage` since 0.3.0, so the shape is generated rather
79
+ * than mirrored here. Anchors are `created_at` timestamps.
80
+ */
81
+ reviews(
82
+ params: ReviewListParams,
83
+ options?: { readonly signal?: AbortSignal }
84
+ ): Promise<ReviewPage>;
85
+
86
+ /**
87
+ * The module-owned rating aggregate for ONE target — mean and count over
88
+ * published reviews. `AllowAny`.
89
+ *
90
+ * There is no batch form on the HTTP surface: `reviews.aggregates_by_keys`
91
+ * is a comm Function for server-side projections, not an endpoint. A
92
+ * storefront that needs a rating per card gets it from the listing rows its
93
+ * own module serves (fed by the composite's projection), not by firing one
94
+ * of these per card.
95
+ */
96
+ aggregate(
97
+ target: ReviewTarget,
98
+ options?: { readonly signal?: AbortSignal }
99
+ ): Promise<ReviewAggregate>;
100
+
101
+ /**
102
+ * Write a review of a target. The one operation here that needs a real
103
+ * identity (`IsAuthenticatedOrReadOnly` refuses an anonymous POST).
104
+ *
105
+ * Refusals worth knowing by name, because their STATUS is not what a reader
106
+ * expects: "you have already reviewed this" is
107
+ * `error.400.reviews_duplicate_review` — a **400**, while the module's only
108
+ * 409 (`error.409.reviews_already_responded`) is about the owner's reply.
109
+ * Branching on the status code would therefore catch the wrong refusal;
110
+ * `model/submit.ts` branches on the code.
111
+ */
112
+ createReview(
113
+ body: {
114
+ readonly targetType: string;
115
+ readonly targetKey: string;
116
+ readonly rating: number;
117
+ readonly body?: string;
118
+ },
119
+ options?: { readonly signal?: AbortSignal }
120
+ ): Promise<Review>;
121
+ }
122
+
123
+ const signalOf = (options?: {
124
+ readonly signal?: AbortSignal;
125
+ }): { signal?: AbortSignal } =>
126
+ options?.signal !== undefined ? { signal: options.signal } : {};
127
+
128
+ export function createReviewsApi(client: StapelClient): ReviewsApi {
129
+ return {
130
+ client,
131
+
132
+ reviews: (params, options) =>
133
+ client.get("/reviews", {
134
+ query: {
135
+ ...targetQuery(params),
136
+ ...(params.include !== undefined ? { include: params.include } : {}),
137
+ ...(params.anchor !== undefined ? { anchor: params.anchor } : {}),
138
+ ...(params.direction !== undefined
139
+ ? { direction: params.direction }
140
+ : {}),
141
+ ...(params.limit !== undefined ? { limit: params.limit } : {}),
142
+ },
143
+ ...signalOf(options),
144
+ }),
145
+
146
+ aggregate: (target, options) =>
147
+ client.get("/reviews/aggregate", {
148
+ query: targetQuery(target),
149
+ ...signalOf(options),
150
+ }),
151
+
152
+ createReview: (body, options) =>
153
+ client.post(
154
+ "/reviews",
155
+ {
156
+ target_type: body.targetType,
157
+ target_key: body.targetKey,
158
+ rating: body.rating,
159
+ // The body defaults to `""` in the request DTO
160
+ // (`dto.ReviewCreateRequest.body: str = ""`) — a rating with no
161
+ // words is a complete review here. Send the field only when there
162
+ // is text, so an empty textarea is not spelled two ways.
163
+ ...(body.body !== undefined && body.body.length > 0
164
+ ? { body: body.body }
165
+ : {}),
166
+ },
167
+ mutating(signalOf(options))
168
+ ),
169
+ };
170
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * The pair's public wire types — projected from the GENERATED schema
3
+ * (`api/generated/schema.ts`, emitted by `pnpm gen:api` from stapel-reviews'
4
+ * own `docs/schema.json`), never re-typed by hand.
5
+ *
6
+ * **Two hand-written shapes lived here against the 0.2.2 pin and are gone.**
7
+ * `ReviewPage` and the list's `anchor`/`limit`/`direction` parameters were
8
+ * declared by this file because `ReviewListCreateView` is a bare `APIView`
9
+ * that instantiates its paginator inside `get()`: drf-spectacular's pagination
10
+ * introspection only fires for `GenericAPIView.pagination_class`, so the
11
+ * schema declared `200: ReviewResponse[]` and knew nothing of the three query
12
+ * parameters. **stapel-reviews 0.3.0 declares both** — `components/ReviewPage`
13
+ * and the parameters, `direction` with an enum — so the copies were deleted
14
+ * and everything below comes from codegen. The upstream ask that produced this
15
+ * is recorded in `contract-pins.json`.
16
+ *
17
+ * Two corrections remain, and both are widenings of what the wire can say
18
+ * rather than shapes the schema is missing:
19
+ *
20
+ * 1. **`status` is a bare `string` on the wire** and a three-member lifecycle
21
+ * in the model (`ReviewStatus`). {@link ReviewStatus} names the three
22
+ * WITHOUT narrowing a parsed value: a fourth state added upstream must
23
+ * render as an unknown state, not crash a switch.
24
+ * 2. **`avg` is `0.0` when `count` is `0`.** The type cannot express that, so
25
+ * {@link RatingAggregate} exists as the shape both this module's own
26
+ * aggregate endpoint AND the shop composite's `shop.listing_review_summary`
27
+ * projection answer with — and `model/rating.ts` is the one place allowed
28
+ * to decide what a zero means.
29
+ */
30
+ import type { components, operations } from "./generated/schema.js";
31
+
32
+ /** Every component schema stapel-reviews declares. */
33
+ export type Schemas = components["schemas"];
34
+
35
+ /** One review of an opaque target, with the owner's reply when there is one. */
36
+ export type Review = Schemas["ReviewResponse"];
37
+
38
+ /** The target owner's single reply to a review. */
39
+ export type ReviewOwnerResponse = Schemas["ResponseResponse"];
40
+
41
+ /** `POST /reviews` request body. */
42
+ export type ReviewCreateRequest = Schemas["ReviewCreateRequest"];
43
+
44
+ /**
45
+ * `GET /reviews/aggregate` 200 body — the module-owned aggregate for ONE
46
+ * target, carrying the target it is about.
47
+ */
48
+ export type ReviewAggregate = Schemas["AggregateResponse"];
49
+
50
+ /**
51
+ * The 200 body of `GET /reviews` — core's `AnchorPagination` envelope,
52
+ * generated since 0.3.0 (see this module's header).
53
+ *
54
+ * `count` is the number of rows in THIS page, not the total; the total number
55
+ * of published reviews is the aggregate's `count`, which is a different number
56
+ * computed a different way.
57
+ */
58
+ export type ReviewPage = Schemas["ReviewPage"];
59
+
60
+ /** The generated query surface of `GET /reviews`. */
61
+ type ReviewListQuery = NonNullable<
62
+ operations["reviews_api_v1_reviews_retrieve"]["parameters"]["query"]
63
+ >;
64
+
65
+ /**
66
+ * A review's visibility, as `models.ReviewStatus` spells it.
67
+ *
68
+ * - `published` — visible to everyone and counted in the aggregate.
69
+ * - `pending` — created under PRE-moderation; invisible until a moderator
70
+ * publishes it, **including to its own author**, who therefore cannot see
71
+ * the review they just wrote in the published-only list.
72
+ * - `hidden` — moderated out; invisible and excluded from the aggregate.
73
+ *
74
+ * Only reachable in a list response when the caller asked for `include=all`
75
+ * AND the target type's `can_moderate` callback said yes; a non-moderator's
76
+ * `include=all` is silently narrowed to published (`views.py`).
77
+ */
78
+ export type ReviewStatus = "pending" | "published" | "hidden";
79
+
80
+ /**
81
+ * The two halves of the module's opaque address. There is no foreign key
82
+ * anywhere in stapel-reviews: `target_type` is a key the HOST registered in
83
+ * `STAPEL_REVIEWS["TARGET_TYPES"]` (the built-in registry is EMPTY) and
84
+ * `target_key` is a host-owned string the module stores, groups by, and never
85
+ * parses.
86
+ *
87
+ * This package therefore ships no target-type constants — not even
88
+ * `"listing"`. The catalogue shop composite registers that name in its own
89
+ * preset (`stapel_shop/preset.py`), a different deployment registers
90
+ * `"seller"` or `"course"`, and a library that guessed would be wrong for
91
+ * every host but one.
92
+ */
93
+ export interface ReviewTarget {
94
+ readonly targetType: string;
95
+ readonly targetKey: string;
96
+ }
97
+
98
+ /** The rating roll-up itself, without the target it belongs to.
99
+ *
100
+ * Deliberately the same two field names the composite's projection uses
101
+ * (`shop.listing_review_summary` → `{avg, count}`, `stapel_shop/models.py`):
102
+ * a seller-level rating is computed by the composite and handed to this
103
+ * pair's display as data, because stapel-reviews can only aggregate ONE
104
+ * `(target_type, target_key)` at a time and has no roll-up endpoint. Same
105
+ * shape in, same rendering out — see {@link ratingSummary}. */
106
+ export interface RatingAggregate {
107
+ /** Mean rating over PUBLISHED reviews. `0` when `count` is `0` — which is
108
+ * "nobody has rated this", never "everyone rated it zero". */
109
+ readonly avg: number;
110
+ /** Number of published reviews. */
111
+ readonly count: number;
112
+ }
113
+
114
+ /**
115
+ * Anchor-pagination direction — the enum the schema now carries. The review
116
+ * list is ordered `-created_at`, so:
117
+ *
118
+ * - `next` (default) — reviews OLDER than the anchor (the load-more direction),
119
+ * - `prev` — reviews NEWER than the anchor,
120
+ * - `center` — a window around the anchor.
121
+ */
122
+ export type ReviewAnchorDirection = NonNullable<ReviewListQuery["direction"]>;
123
+
124
+ /**
125
+ * Query for `GET /reviews`: the generated parameters, with only the target
126
+ * pair renamed to this package's camelCase {@link ReviewTarget}. Nothing here
127
+ * is hand-typed any more — `include`, `anchor`, `limit` and `direction` are
128
+ * exactly what the contract declares.
129
+ *
130
+ * `target_type`/`target_key` are required by the view (a missing one is
131
+ * `error.400.reviews_unknown_target_type`, not an empty list). `anchor` is a
132
+ * `created_at` ISO timestamp and exclusive. `include` is typed `string`
133
+ * because the schema declares no enum for it: the view acts on the literal
134
+ * `"all"` and treats anything else as published-only, silently — which is why
135
+ * the hook option that offers it (`UseReviewListOptions.include`) narrows to
136
+ * the one value the server actually reads.
137
+ */
138
+ export interface ReviewListParams
139
+ extends ReviewTarget,
140
+ Omit<ReviewListQuery, "target_type" | "target_key"> {}