@rebasepro/agent-skills 0.9.1-canary.ff338b5 → 0.10.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/package.json +1 -1
- package/skills/rebase-auth/SKILL.md +41 -3
- package/skills/rebase-backend-postgres/SKILL.md +5 -4
- package/skills/rebase-collections/SKILL.md +3 -3
- package/skills/rebase-deployment/SKILL.md +2 -1
- package/skills/rebase-design-language/SKILL.md +6 -6
- package/skills/rebase-email/SKILL.md +4 -4
- package/skills/rebase-realtime/SKILL.md +66 -7
- package/skills/rebase-sdk/SKILL.md +25 -17
- package/skills/rebase-security/SKILL.md +1 -1
- package/skills/rebase-storage/SKILL.md +1 -1
- package/skills/rebase-ui-components/SKILL.md +6 -6
package/package.json
CHANGED
|
@@ -219,11 +219,48 @@ Twitter uses OAuth 2.0 with PKCE. The client must send `codeVerifier` alongside
|
|
|
219
219
|
|
|
220
220
|
### OAuth Account Linking
|
|
221
221
|
|
|
222
|
-
When an OAuth user signs in
|
|
223
|
-
|
|
224
|
-
|
|
222
|
+
When an OAuth user signs in via `POST /api/auth/{provider}`:
|
|
223
|
+
|
|
224
|
+
1. If an identity record exists for `(provider, providerId)` → log in that user. The email is not consulted.
|
|
225
|
+
2. If no identity exists but a user with the same email exists:
|
|
226
|
+
- **The provider asserted `emailVerified: true`** → **link** the provider to the existing account and log in as that user. One account, two sign-in methods.
|
|
227
|
+
- **The provider did NOT verify the email** → reject with `403 EMAIL_NOT_VERIFIED`. Nothing is created or modified.
|
|
225
228
|
3. If neither exists → create a new user, link the identity, assign `defaultRole`.
|
|
226
229
|
|
|
230
|
+
> **IMPORTANT FOR AGENTS:** A second account is **never** silently created for
|
|
231
|
+
> an email that already exists. If asked "does signing in with Google create a
|
|
232
|
+
> duplicate user?", the answer is no — it either links (verified) or errors
|
|
233
|
+
> (unverified). This is **not configurable**; there is deliberately no option to
|
|
234
|
+
> auto-link on unverified emails, because that would let anyone who can make a
|
|
235
|
+
> provider emit an address they don't own take over the matching account.
|
|
236
|
+
> Google always asserts `email_verified` for real Google accounts, so linking
|
|
237
|
+
> is the normal path for Google sign-in.
|
|
238
|
+
|
|
239
|
+
### Linking a Provider to a Signed-In Account
|
|
240
|
+
|
|
241
|
+
`POST /api/auth/link/{provider}` attaches a provider identity to the **already
|
|
242
|
+
authenticated** account (requires `Authorization: Bearer <token>`). The body is
|
|
243
|
+
the same payload the provider's sign-in route takes, e.g. `{ idToken }`.
|
|
244
|
+
|
|
245
|
+
This is the escape hatch from an `EMAIL_NOT_VERIFIED` rejection, and the way to
|
|
246
|
+
attach a provider whose email differs from the account's.
|
|
247
|
+
|
|
248
|
+
Unlike sign-in, linking here does **not** require a verified email and does not
|
|
249
|
+
require the emails to match — on sign-in the provider's email is the only
|
|
250
|
+
evidence tying the identity to an account, whereas here the caller has already
|
|
251
|
+
proven ownership by holding a valid session.
|
|
252
|
+
|
|
253
|
+
- `409 IDENTITY_ALREADY_LINKED` if that provider identity belongs to another user.
|
|
254
|
+
- Idempotent (`alreadyLinked: true`) if already linked to the caller.
|
|
255
|
+
|
|
256
|
+
### Adding a Password to a Provider-Only Account
|
|
257
|
+
|
|
258
|
+
A user who signed up via OAuth has no `passwordHash`:
|
|
259
|
+
|
|
260
|
+
- `POST /auth/register` with the same email → `409 EMAIL_EXISTS`.
|
|
261
|
+
- `POST /auth/change-password` → `400 INVALID_ACCOUNT` (no existing password to verify).
|
|
262
|
+
- **`forgot-password` → `reset-password` is the supported path.** It re-proves ownership of the address by email, after which the account has both sign-in methods.
|
|
263
|
+
|
|
227
264
|
### Custom OAuth Provider
|
|
228
265
|
|
|
229
266
|
You can register any OAuth provider by implementing the `OAuthProvider<T>` interface:
|
|
@@ -659,6 +696,7 @@ All auth endpoints are mounted under `/api/auth`. Admin endpoints are under `/ap
|
|
|
659
696
|
| `PATCH` | `/auth/me` | Required | Update profile. Body: `{ displayName?, photoURL? }`. |
|
|
660
697
|
| `POST` | `/auth/change-password` | Required | Change password. Body: `{ oldPassword, newPassword }`. Invalidates all sessions. |
|
|
661
698
|
| `POST` | `/auth/send-verification` | Required | Send email verification link. Requires email service. |
|
|
699
|
+
| `POST` | `/auth/link/{provider}` | Required | Link an OAuth provider to the current account. Body: the provider's sign-in payload (e.g. `{ idToken }`). `409 IDENTITY_ALREADY_LINKED` if it belongs to another user. |
|
|
662
700
|
| `GET` | `/auth/sessions` | Required | List active sessions (refresh tokens). |
|
|
663
701
|
| `DELETE` | `/auth/sessions` | Required | Revoke all sessions (remote logout). |
|
|
664
702
|
| `DELETE` | `/auth/sessions/:id` | Required | Revoke a specific session. |
|
|
@@ -95,7 +95,7 @@ const { db, pool, connectionString } = createPostgresDatabaseConnection(
|
|
|
95
95
|
|
|
96
96
|
**Signature:**
|
|
97
97
|
|
|
98
|
-
```typescript
|
|
98
|
+
```typescript no-verify
|
|
99
99
|
function createPostgresDatabaseConnection(
|
|
100
100
|
connectionString: string,
|
|
101
101
|
schema?: Record<string, unknown>,
|
|
@@ -120,7 +120,7 @@ const readResources = createReadReplicaConnection(
|
|
|
120
120
|
|
|
121
121
|
**Signature:**
|
|
122
122
|
|
|
123
|
-
```typescript
|
|
123
|
+
```typescript no-verify
|
|
124
124
|
function createReadReplicaConnection(
|
|
125
125
|
connectionString: string,
|
|
126
126
|
schema?: Record<string, unknown>,
|
|
@@ -145,7 +145,7 @@ const directResources = createDirectDatabaseConnection(
|
|
|
145
145
|
|
|
146
146
|
**Signature:**
|
|
147
147
|
|
|
148
|
-
```typescript
|
|
148
|
+
```typescript no-verify
|
|
149
149
|
function createDirectDatabaseConnection(
|
|
150
150
|
connectionString: string,
|
|
151
151
|
schema?: Record<string, unknown>,
|
|
@@ -387,7 +387,8 @@ export const env = loadEnv();
|
|
|
387
387
|
|
|
388
388
|
```typescript
|
|
389
389
|
import dotenv from "dotenv";
|
|
390
|
-
import { loadEnv
|
|
390
|
+
import { loadEnv } from "@rebasepro/server";
|
|
391
|
+
import { z } from "zod";
|
|
391
392
|
|
|
392
393
|
dotenv.config({ path: "../../.env" });
|
|
393
394
|
|
|
@@ -495,7 +495,7 @@ validation: {
|
|
|
495
495
|
|
|
496
496
|
Maps store nested objects as `JSONB` in PostgreSQL. They can define their own inner properties schema.
|
|
497
497
|
|
|
498
|
-
```typescript
|
|
498
|
+
```typescript no-verify
|
|
499
499
|
address: {
|
|
500
500
|
name: "Address",
|
|
501
501
|
type: "map",
|
|
@@ -663,7 +663,7 @@ Every property supports a `validation` object with these common options:
|
|
|
663
663
|
|
|
664
664
|
Use the `enum` property on `string` or `number` types to define picklist options:
|
|
665
665
|
|
|
666
|
-
```typescript
|
|
666
|
+
```typescript no-verify
|
|
667
667
|
status: {
|
|
668
668
|
name: "Status",
|
|
669
669
|
type: "string",
|
|
@@ -687,7 +687,7 @@ Each `EnumValueConfig` entry supports:
|
|
|
687
687
|
|
|
688
688
|
`EnumValues` can also be a `Record<string, string | EnumValueConfig>` for simpler definitions:
|
|
689
689
|
|
|
690
|
-
```typescript
|
|
690
|
+
```typescript no-verify
|
|
691
691
|
enum: {
|
|
692
692
|
draft: "Draft",
|
|
693
693
|
published: { label: "Published", color: "green" }
|
|
@@ -348,7 +348,8 @@ Use the `extend` option to add your own typed env variables on top of the base R
|
|
|
348
348
|
|
|
349
349
|
```typescript
|
|
350
350
|
import dotenv from "dotenv";
|
|
351
|
-
import { loadEnv
|
|
351
|
+
import { loadEnv } from "@rebasepro/server";
|
|
352
|
+
import { z } from "zod";
|
|
352
353
|
|
|
353
354
|
dotenv.config({ path: "../../.env" });
|
|
354
355
|
|
|
@@ -202,7 +202,7 @@ The typography scale is compact and uses semibold weights for headings — desig
|
|
|
202
202
|
|
|
203
203
|
### Typography Anti-Patterns
|
|
204
204
|
|
|
205
|
-
```tsx
|
|
205
|
+
```tsx no-verify
|
|
206
206
|
// ❌ NEVER: Gradient text on headings
|
|
207
207
|
<Typography className="bg-gradient-to-r from-violet-600 to-cyan-500 bg-clip-text text-transparent">
|
|
208
208
|
|
|
@@ -270,7 +270,7 @@ The `Card` component from `@rebasepro/ui` automatically applies `cardMixin` and,
|
|
|
270
270
|
// cardClickableMixin = "hover:bg-surface-50 dark:hover:bg-surface-800 cursor-pointer transition-colors duration-150"
|
|
271
271
|
```
|
|
272
272
|
|
|
273
|
-
```tsx
|
|
273
|
+
```tsx no-verify
|
|
274
274
|
// ✅ CORRECT: Using Card component
|
|
275
275
|
<Card className="p-4">
|
|
276
276
|
<Typography variant="subtitle1">Title</Typography>
|
|
@@ -407,7 +407,7 @@ The `Button` component accepts these variants and colors. Buttons use `rounded-l
|
|
|
407
407
|
|
|
408
408
|
### Button Anti-Patterns
|
|
409
409
|
|
|
410
|
-
```tsx
|
|
410
|
+
```tsx no-verify
|
|
411
411
|
// ❌ NEVER: Casting variant to bypass types
|
|
412
412
|
<Button variant={"standard" as any}>
|
|
413
413
|
|
|
@@ -432,7 +432,7 @@ The `Button` component accepts these variants and colors. Buttons use `rounded-l
|
|
|
432
432
|
|
|
433
433
|
### Use the `Alert` Component
|
|
434
434
|
|
|
435
|
-
```tsx
|
|
435
|
+
```tsx no-verify
|
|
436
436
|
// ✅ CORRECT
|
|
437
437
|
<Alert color="success" size="small">Lead captured successfully!</Alert>
|
|
438
438
|
<Alert color="error">Could not load dashboard statistics.</Alert>
|
|
@@ -508,7 +508,7 @@ When displaying numeric KPIs, use plain muted icons (matching NavigationCard)
|
|
|
508
508
|
|
|
509
509
|
### Metric Card Anti-Patterns
|
|
510
510
|
|
|
511
|
-
```tsx
|
|
511
|
+
```tsx no-verify
|
|
512
512
|
// ❌ NEVER: Gradient icon backgrounds
|
|
513
513
|
<div className="p-4 bg-gradient-to-br from-blue-500 to-indigo-600 text-white rounded-2xl shadow-lg shadow-blue-500/20">
|
|
514
514
|
|
|
@@ -555,7 +555,7 @@ For larger content sections (like a table, form, or embedded panel), use `Paper`
|
|
|
555
555
|
|
|
556
556
|
### Panel Anti-Patterns
|
|
557
557
|
|
|
558
|
-
```tsx
|
|
558
|
+
```tsx no-verify
|
|
559
559
|
// ❌ WRONG: Custom panel styling
|
|
560
560
|
<div className="p-6 rounded-2xl border bg-white dark:bg-surface-900/50 backdrop-blur-md shadow-sm">
|
|
561
561
|
|
|
@@ -217,7 +217,7 @@ import {
|
|
|
217
217
|
|
|
218
218
|
#### getPasswordResetTemplate
|
|
219
219
|
|
|
220
|
-
```typescript
|
|
220
|
+
```typescript no-verify
|
|
221
221
|
function getPasswordResetTemplate(
|
|
222
222
|
resetUrl: string,
|
|
223
223
|
user: { email: string; displayName?: string | null },
|
|
@@ -229,7 +229,7 @@ The template includes a "Reset Password" button linking to `resetUrl`, a plain-t
|
|
|
229
229
|
|
|
230
230
|
#### getEmailVerificationTemplate
|
|
231
231
|
|
|
232
|
-
```typescript
|
|
232
|
+
```typescript no-verify
|
|
233
233
|
function getEmailVerificationTemplate(
|
|
234
234
|
verifyUrl: string,
|
|
235
235
|
user: { email: string; displayName?: string | null },
|
|
@@ -241,7 +241,7 @@ The template includes a "Verify Email Address" button linking to `verifyUrl`.
|
|
|
241
241
|
|
|
242
242
|
#### getUserInvitationTemplate
|
|
243
243
|
|
|
244
|
-
```typescript
|
|
244
|
+
```typescript no-verify
|
|
245
245
|
function getUserInvitationTemplate(
|
|
246
246
|
setPasswordUrl: string,
|
|
247
247
|
user: { email: string; displayName?: string | null },
|
|
@@ -253,7 +253,7 @@ Sent when an admin creates a user via `POST /api/admin/users` without specifying
|
|
|
253
253
|
|
|
254
254
|
#### getWelcomeEmailTemplate
|
|
255
255
|
|
|
256
|
-
```typescript
|
|
256
|
+
```typescript no-verify
|
|
257
257
|
function getWelcomeEmailTemplate(
|
|
258
258
|
user: { email: string; displayName?: string | null },
|
|
259
259
|
appName?: string, // default: "Rebase"
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rebase-realtime
|
|
3
|
-
description: Guide for the Rebase WebSocket realtime engine. Use this skill when the user needs real-time data synchronization, broadcast channels, presence tracking, table change broadcasts, or live updates in their application.
|
|
3
|
+
description: Guide for the Rebase WebSocket realtime engine. Use this skill when the user needs real-time data synchronization, broadcast channels, presence tracking, channel message history and reconnect catch-up (collaborative/op-based editing), table change broadcasts, or live updates in their application.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Rebase Realtime Engine
|
|
7
7
|
|
|
8
|
-
Rebase includes a built-in WebSocket-based realtime engine that broadcasts database changes, supports broadcast channels for arbitrary client-to-client messaging, and provides presence tracking for online status.
|
|
8
|
+
Rebase includes a built-in WebSocket-based realtime engine that broadcasts database changes, supports broadcast channels for arbitrary client-to-client messaging (optionally with replayable message history), and provides presence tracking for online status.
|
|
9
9
|
|
|
10
10
|
> **IMPORTANT FOR AGENTS:** The WebSocket protocol uses specific message types that differ from typical pub/sub patterns. Read the **Message Types** section carefully before generating any WebSocket code. Do NOT invent message type names — use only the types documented here.
|
|
11
11
|
|
|
@@ -68,8 +68,8 @@ Pass `FindParams` as the first argument to filter the subscription:
|
|
|
68
68
|
```typescript
|
|
69
69
|
const unsubscribe = client.data.products.listen(
|
|
70
70
|
{
|
|
71
|
-
where: { status: "published" },
|
|
72
|
-
orderBy: "created_at
|
|
71
|
+
where: { status: ["==", "published"] },
|
|
72
|
+
orderBy: ["created_at", "desc"],
|
|
73
73
|
limit: 50,
|
|
74
74
|
},
|
|
75
75
|
(response) => {
|
|
@@ -120,7 +120,7 @@ const unsubscribe = client.data.products.listenById(
|
|
|
120
120
|
|
|
121
121
|
### `listenById()` Signature
|
|
122
122
|
|
|
123
|
-
```typescript
|
|
123
|
+
```typescript no-verify
|
|
124
124
|
listenById(
|
|
125
125
|
id: string | number,
|
|
126
126
|
onUpdate: (entity: Entity<M> | undefined) => void,
|
|
@@ -148,7 +148,7 @@ In React, use `useEffect` cleanup:
|
|
|
148
148
|
```tsx
|
|
149
149
|
useEffect(() => {
|
|
150
150
|
const unsubscribe = client.data.products.listen(
|
|
151
|
-
{ where: { active: true } },
|
|
151
|
+
{ where: { active: ["==", true] } },
|
|
152
152
|
(response) => setProducts(response.data)
|
|
153
153
|
);
|
|
154
154
|
return () => unsubscribe();
|
|
@@ -342,6 +342,7 @@ ws.onmessage = (event) => {
|
|
|
342
342
|
| `presence_track` | `{ channel, state }` | Track presence with custom state (auto-joins channel) |
|
|
343
343
|
| `presence_untrack` | `{ channel }` | Stop tracking presence |
|
|
344
344
|
| `presence_state` | `{ channel }` | Request full presence entity |
|
|
345
|
+
| `channel_history` | `{ channel, sinceSeq?, limit? }` | Request retained messages after `sinceSeq` (retained channels only) |
|
|
345
346
|
| `FETCH_COLLECTION` | `FetchCollectionProps` | One-shot collection fetch (request/response) |
|
|
346
347
|
| `FETCH_ENTITY` | `FetchEntityProps` | One-shot entity fetch |
|
|
347
348
|
| `SAVE_ENTITY` | `SaveEntityProps` | Create or update a entity |
|
|
@@ -358,9 +359,10 @@ ws.onmessage = (event) => {
|
|
|
358
359
|
| `collection_update` | `{ subscriptionId, entities }` | Full collection data (authoritative refetch) |
|
|
359
360
|
| `entity_update` | `{ subscriptionId, entity }` | Single entity data (entity or `null` if deleted) |
|
|
360
361
|
| `collection_entity_patch` | `{ subscriptionId, entityId, entity }` | Instant single-entity patch for a collection subscription |
|
|
361
|
-
| `broadcast` | `{ channel, event, payload }` | Broadcast from another channel member |
|
|
362
|
+
| `broadcast` | `{ channel, event, payload, seq? }` | Broadcast from another channel member. `seq` is present only on retained channels |
|
|
362
363
|
| `presence_state` | `{ channel, presences }` | Full presence entity |
|
|
363
364
|
| `presence_diff` | `{ channel, joins, leaves }` | Incremental presence update |
|
|
365
|
+
| `channel_history` | `{ channel, messages, retained, latestSeq? }` | Retained messages a client missed. `retained: false` means the channel keeps no history |
|
|
364
366
|
| `ERROR` | `{ requestId?, payload: { error: { message, code } } }` | General error |
|
|
365
367
|
| `FETCH_COLLECTION_SUCCESS` | `{ requestId, payload: { entities } }` | Response to FETCH_COLLECTION |
|
|
366
368
|
| `FETCH_ENTITY_SUCCESS` | `{ requestId, payload: { entity } }` | Response to FETCH_ENTITY |
|
|
@@ -449,6 +451,63 @@ ws.send(JSON.stringify({
|
|
|
449
451
|
|
|
450
452
|
When a client disconnects, they are automatically removed from all channels.
|
|
451
453
|
|
|
454
|
+
## Channel History and Catch-Up
|
|
455
|
+
|
|
456
|
+
By default a broadcast reaches currently-connected members and is then gone — correct for cursors and notifications, and it costs nothing. A channel can instead be configured to **retain** its messages, so a reconnecting client catches up on what it missed rather than resyncing. This is what makes channels usable for operation streams (collaborative editing).
|
|
457
|
+
|
|
458
|
+
> **IMPORTANT FOR AGENTS:** Retention is configured on the **server** and cannot be requested by a client. `{ history: true }` on the client only asks the SDK to *use* history for a channel the server already retains. If the user reports "history isn't working", check the server config first — the symptom of a missing rule is `retained: false`, not an error.
|
|
459
|
+
|
|
460
|
+
### Server config (required)
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
createPostgresAdapter({
|
|
464
|
+
connection: db,
|
|
465
|
+
schema: { tables, enums, relations },
|
|
466
|
+
realtime: {
|
|
467
|
+
channels: [
|
|
468
|
+
// Most specific first — first match wins.
|
|
469
|
+
{ match: "doc:*", limit: 500, ttl: "24h" }
|
|
470
|
+
]
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
`match` is an exact channel name or a trailing-`*` prefix — **not** a general glob (`"doc:*:live"` matches nothing). A rule needs at least one of `limit` or `ttl`, or it is ignored and logged; unbounded retention is refused rather than honoured.
|
|
476
|
+
|
|
477
|
+
`ttl` accepts `"30s"`, `"15m"`, `"24h"`, `"7d"`, or a millisecond number. An unparseable value is ignored with a warning rather than guessed at.
|
|
478
|
+
|
|
479
|
+
### Client usage
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
const channel = client.realtime.channel("doc:42", { history: true });
|
|
483
|
+
|
|
484
|
+
// Replayed messages arrive through the SAME handler, in order.
|
|
485
|
+
channel.onBroadcast("op", (payload) => applyOperation(payload));
|
|
486
|
+
await channel.join();
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
The SDK requests catch-up on `join()` and after every reconnect, resuming from the highest `seq` it has delivered. Explicit fetch:
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
const { messages, retained, latestSeq } = await channel.history({ sinceSeq, limit });
|
|
493
|
+
console.log(channel.sequence); // highest seq delivered so far
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Behaviour worth knowing
|
|
497
|
+
|
|
498
|
+
- **`seq` is per-channel, dense and increasing.** It appears on `BroadcastEvent.seq` only for retained channels; ephemeral broadcasts have no `seq`.
|
|
499
|
+
- **`replayed: true`** marks a message delivered by catch-up rather than live. Handlers usually ignore it.
|
|
500
|
+
- **Replays overlap, and duplicates are dropped by the SDK.** Anything at or below the last delivered sequence is discarded, so handlers never see a message twice.
|
|
501
|
+
- **Your own messages are not filtered out of a replay.** A reconnect assigns a new client id, so filtering by sender would fail in exactly the case catch-up exists for. Operations must be idempotent.
|
|
502
|
+
- **A message that cannot be persisted is not delivered**, and the sender gets a `CHANNEL_HISTORY_WRITE_FAILED` error. Delivering it would leave a gap no replay could repair.
|
|
503
|
+
- **Live messages are held back during catch-up** so ordering holds, bounded by a 10s timeout.
|
|
504
|
+
|
|
505
|
+
### Storage
|
|
506
|
+
|
|
507
|
+
Two tables in the `rebase` schema, created on startup only when a rule exists: `rebase.channel_messages` (keyed `(channel, seq)`) and `rebase.channel_cursors`. Pruning removes messages only — cursors are kept forever, because restarting a sequence would change what a client's saved resume point means.
|
|
508
|
+
|
|
509
|
+
> **Security:** history has the same access model as the channel. Anyone who may join may replay what was said before they arrived. Enabling retention on a publicly-joinable channel makes its past readable to any visitor.
|
|
510
|
+
|
|
452
511
|
## Presence Tracking
|
|
453
512
|
|
|
454
513
|
Presence tracks which users are currently online in a channel, with custom state per user.
|
|
@@ -150,7 +150,7 @@ await rebase.data.posts.delete('post-123');
|
|
|
150
150
|
|
|
151
151
|
// Count
|
|
152
152
|
const total = await rebase.data.posts.count({
|
|
153
|
-
where: { status: '
|
|
153
|
+
where: { status: ['==', 'published'] },
|
|
154
154
|
});
|
|
155
155
|
```
|
|
156
156
|
|
|
@@ -177,31 +177,39 @@ Pass a params object directly to `.find()`:
|
|
|
177
177
|
const result = await rebase.data.posts.find({
|
|
178
178
|
limit: 20,
|
|
179
179
|
offset: 0,
|
|
180
|
-
orderBy: 'createdAt
|
|
180
|
+
orderBy: ['createdAt', 'desc'],
|
|
181
181
|
searchString: 'search term',
|
|
182
182
|
include: ['tags', 'author'], // or ['*'] for all relations
|
|
183
183
|
where: {
|
|
184
|
-
status: '
|
|
185
|
-
price: '
|
|
186
|
-
category: 'in
|
|
184
|
+
status: ['==', 'published'],
|
|
185
|
+
price: ['>=', 100],
|
|
186
|
+
category: ['in', ['electronics', 'books']],
|
|
187
187
|
},
|
|
188
188
|
});
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
-
### Filter Operators (
|
|
191
|
+
### Filter Operators (Tuple Syntax)
|
|
192
|
+
|
|
193
|
+
Every filter is a `[operator, value]` tuple. The bare-string and
|
|
194
|
+
`'eq.published'` wire formats are **not** part of the public API: they are
|
|
195
|
+
`WireFilterValues`, marked `@internal`, and `serializeFilter` passes such a
|
|
196
|
+
string straight through to PostgREST unchanged — so an operator-less value
|
|
197
|
+
silently produces a malformed query rather than an error.
|
|
192
198
|
|
|
193
199
|
| Operator | Description | Example |
|
|
194
200
|
|----------|-------------|---------|
|
|
195
|
-
|
|
|
196
|
-
|
|
|
197
|
-
|
|
|
198
|
-
|
|
|
199
|
-
|
|
|
200
|
-
|
|
|
201
|
-
| `in` | In list | `'in
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
201
|
+
| `==` | Equal | `['==', 'published']` |
|
|
202
|
+
| `!=` | Not equal | `['!=', 'draft']` |
|
|
203
|
+
| `>` | Greater than | `['>', 100]` |
|
|
204
|
+
| `>=` | Greater than or equal | `['>=', 100]` |
|
|
205
|
+
| `<` | Less than | `['<', 50]` |
|
|
206
|
+
| `<=` | Less than or equal | `['<=', 50]` |
|
|
207
|
+
| `in` | In list | `['in', ['electronics', 'books']]` |
|
|
208
|
+
| `not-in` | Not in list | `['not-in', ['archived', 'deleted']]` |
|
|
209
|
+
| `array-contains` | Array contains | `['array-contains', 'tag1']` |
|
|
210
|
+
| `array-contains-any` | Array contains any | `['array-contains-any', ['tag1', 'tag2']]` |
|
|
211
|
+
| `like` / `ilike` | Pattern match (SQL wildcards) | `['ilike', '%john%']` |
|
|
212
|
+
| `is-null` / `is-not-null` | Null checks (value ignored) | `['is-null', null]` |
|
|
205
213
|
|
|
206
214
|
### Approach 2: Fluent QueryBuilder
|
|
207
215
|
|
|
@@ -352,7 +360,7 @@ unsubscribe();
|
|
|
352
360
|
```typescript
|
|
353
361
|
// Via CollectionClient
|
|
354
362
|
const unsubscribe = rebase.data.posts.listen(
|
|
355
|
-
{ where: { status: 'published' }, limit: 50 },
|
|
363
|
+
{ where: { status: ['==', 'published'] }, limit: 50 },
|
|
356
364
|
(response) => {
|
|
357
365
|
// response: FindResponse with updated data
|
|
358
366
|
console.log('Updated posts:', response.data);
|
|
@@ -316,7 +316,7 @@ For full documentation on collection callbacks, see the `rebase-collections` ski
|
|
|
316
316
|
Rebase follows a **fail-closed** security model throughout the stack:
|
|
317
317
|
|
|
318
318
|
1. **Scoped driver or nothing** — The REST API's `getScopedDriver()` throws if no scoped driver is available. It **never** falls back to the unscoped driver:
|
|
319
|
-
```typescript
|
|
319
|
+
```typescript no-verify
|
|
320
320
|
private getScopedDriver(c): DataDriver {
|
|
321
321
|
const driver = c.get("driver") as DataDriver | undefined;
|
|
322
322
|
if (!driver) throw ApiError.internal("Scoped driver not available");
|
|
@@ -332,7 +332,7 @@ STORAGE_GCS_BUCKET=my-bucket
|
|
|
332
332
|
```
|
|
333
333
|
|
|
334
334
|
❌ **Anti-pattern (never do this in app/backend code):**
|
|
335
|
-
```typescript
|
|
335
|
+
```typescript no-verify
|
|
336
336
|
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; // ❌
|
|
337
337
|
const s3 = new S3Client({ region, endpoint }); // ❌ hardcodes S3
|
|
338
338
|
await s3.send(new PutObjectCommand({ Bucket, Key, Body })); // ❌ bypasses rebase.storage
|
|
@@ -31,7 +31,7 @@ The package exports five modules:
|
|
|
31
31
|
export * from "./components"; // All UI components
|
|
32
32
|
export * from "./styles"; // Tailwind style mixin strings
|
|
33
33
|
export * from "./util"; // cls(), debounce(), chip_colors, keyToIconComponent
|
|
34
|
-
export * from "./icons"; // Icon component, iconSize, lucideIcons map,
|
|
34
|
+
export * from "./icons"; // Icon component, iconSize, lucideIcons map, iconKeys, cool_icon_keys
|
|
35
35
|
export * from "./hooks"; // React hooks
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -1149,13 +1149,13 @@ const IconComponent = lucideIcons["Database"]; // LucideIcon component
|
|
|
1149
1149
|
<IconComponent size={24} />
|
|
1150
1150
|
```
|
|
1151
1151
|
|
|
1152
|
-
### `
|
|
1152
|
+
### `iconKeys` and `cool_icon_keys`
|
|
1153
1153
|
|
|
1154
|
-
- **`
|
|
1154
|
+
- **`iconKeys`** — Array of ~1900+ all available Lucide icon name strings.
|
|
1155
1155
|
- **`cool_icon_keys`** — Curated subset of ~50 visually distinctive icon names for icon pickers.
|
|
1156
1156
|
|
|
1157
1157
|
```tsx
|
|
1158
|
-
import {
|
|
1158
|
+
import { iconKeys, coolIconKeys, lucideIcons } from "@rebasepro/ui";
|
|
1159
1159
|
```
|
|
1160
1160
|
|
|
1161
1161
|
### `keyToIconComponent(key: string): string`
|
|
@@ -1265,7 +1265,7 @@ debouncedSearch.clear(); // Cancel pending call
|
|
|
1265
1265
|
Deterministic 32-bit hash of a string. Returns a positive integer.
|
|
1266
1266
|
|
|
1267
1267
|
```tsx
|
|
1268
|
-
import { hashString } from "@rebasepro/
|
|
1268
|
+
import { hashString } from "@rebasepro/utils";
|
|
1269
1269
|
hashString("my-entity-id"); // e.g. 1234567
|
|
1270
1270
|
```
|
|
1271
1271
|
|
|
@@ -1280,7 +1280,7 @@ import {
|
|
|
1280
1280
|
```
|
|
1281
1281
|
|
|
1282
1282
|
**`ChipColorScheme`** type:
|
|
1283
|
-
```ts
|
|
1283
|
+
```ts no-verify
|
|
1284
1284
|
{
|
|
1285
1285
|
color: string; // Light mode background hex
|
|
1286
1286
|
text: string; // Light mode text hex
|