// Creates a note. There's NO rate-limit code here — the limit is enforced by // the plugin's rpc interceptor (see app.config.ts). When the caller is over // the limit, the call fails a typed `RateLimited` error that the plugin merges // into this procedure's wire error union automatically — you don't declare it. // (`TenantMismatch` is the explicit error this handler can throw.) import { defineMutation } from '@voltro/protocol' import { TenantMismatch } from '@voltro/plugin-multitenancy/guard' import { Schema } from 'effect' export const createNote = defineMutation({ name: 'notes.create', // A rate limit is not an access decision, and this template is where that is // easiest to get wrong. The interceptor answers HOW OFTEN a caller may do // this; `guards:` / `openAccess:` answer WHETHER THEY MAY AT ALL. A limit // slows an attacker down and lets them through 99 times an hour, so it can // never stand in for the second answer — the two live side by side. // // Open here because no auth strategy and no rbac ship in this template: every // caller resolves to an anonymous Subject holding no scopes, so a // `guards: [{ scope }]` would deny all of them. `assertOwnTenant` still // rejects a `tenantId` that does not match the resolved subject. openAccess: 'inserts a note carrying only what the caller sent, into the caller\'s own tenant ' + '(`assertOwnTenant` rejects a mismatch). The rate limit bounds HOW OFTEN, never WHETHER ' + '— it is not an access decision, which is why this line exists beside it.', target: { table: 'notes', op: 'insert', shape: (input: { tenantId: string; title: string; body: string }) => ({ title: input.title, body: input.body, tenantId: input.tenantId, }), }, input: Schema.Struct({ tenantId: Schema.String, title: Schema.NonEmptyString, body: Schema.String, }), output: Schema.Struct({ id: Schema.String, title: Schema.String, body: Schema.String, tenantId: Schema.String, }), error: TenantMismatch, })