feat(v1.1.5): files SDK + files:* triggers
Filesystem-backed blob storage as the fifth concrete trigger kind.
- `files::collection(c).{create,head,get,update,delete,list}` Rhai SDK
(blob in/out; metadata maps; missing-field throws naming the field).
- `FilesService` trait in picloud-shared; `FsFilesRepo` (atomic
write: temp→fsync→rename→fsync-dir→DB; single-pass SHA-256;
checksum-verified reads → Corrupted) + `FilesServiceImpl` in
manager-core. Metadata in Postgres (0018), bytes on disk under
PICLOUD_FILES_ROOT with 0o700 shard dirs.
- `files:*` trigger kind via the Layout-E pattern (0019: widen both
CHECKs + files_trigger_details), TriggerEvent::Files (metadata only,
no bytes), emit_files fan-out, dispatcher arm, admin endpoint
POST /triggers/files (reuses validate_trigger_target).
- AppFilesRead/AppFilesWrite capabilities → script:read/script:write
(seven-scope commitment held). AppPubsubPublish reserved for v1.1.6.
- Admin files API (list + delete) + dashboard Files view per app.
Cross-app isolation keyed on cx.app_id at every layer. ~45 new tests
(service in-memory, fs tempdir, bridge integration). No DB required
for the suite. publish_ephemeral and the orphan sweep stay deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -211,7 +211,7 @@ export interface DeadLetterRow {
|
||||
resolution: 'replayed' | 'ignored' | 'handled_by_script' | 'handler_failed' | null;
|
||||
}
|
||||
|
||||
export type TriggerKind = 'kv' | 'docs' | 'dead_letter' | 'cron';
|
||||
export type TriggerKind = 'kv' | 'docs' | 'dead_letter' | 'cron' | 'files' | 'pubsub';
|
||||
export type TriggerDispatchMode = 'sync' | 'async';
|
||||
|
||||
/// Per-kind detail, tagged by `kind` to match the Rust serde shape.
|
||||
@@ -219,7 +219,21 @@ export type TriggerDetails =
|
||||
| { kind: 'kv'; collection_glob: string; ops: string[] }
|
||||
| { kind: 'docs'; collection_glob: string; ops: string[] }
|
||||
| { kind: 'dead_letter'; source_filter?: string; trigger_id_filter?: string; script_id_filter?: string }
|
||||
| { kind: 'cron'; schedule: string; timezone: string; last_fired_at?: string | null };
|
||||
| { kind: 'cron'; schedule: string; timezone: string; last_fired_at?: string | null }
|
||||
| { kind: 'files'; collection_glob: string; ops: string[] }
|
||||
| { kind: 'pubsub'; topic_pattern: string };
|
||||
|
||||
/// v1.1.5 file metadata as the admin files endpoint returns it.
|
||||
export interface FileMeta {
|
||||
id: string;
|
||||
collection: string;
|
||||
name: string;
|
||||
content_type: string;
|
||||
size: number;
|
||||
checksum: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Trigger {
|
||||
id: string;
|
||||
@@ -625,6 +639,23 @@ export const api = {
|
||||
)
|
||||
},
|
||||
|
||||
files: {
|
||||
list: (idOrSlug: string, collection: string, opts: { cursor?: string; limit?: number } = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('collection', collection);
|
||||
if (opts.cursor) params.set('cursor', opts.cursor);
|
||||
if (opts.limit !== undefined) params.set('limit', String(opts.limit));
|
||||
return adminRequest<{ files: FileMeta[]; next_cursor: string | null }>(
|
||||
`/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/files?${params.toString()}`
|
||||
);
|
||||
},
|
||||
remove: (idOrSlug: string, collection: string, fileId: string) =>
|
||||
adminRequest<null>(
|
||||
`/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/files/${encodeURIComponent(collection)}/${fileId}`,
|
||||
{ method: 'DELETE' }
|
||||
)
|
||||
},
|
||||
|
||||
execute: async (
|
||||
id: string,
|
||||
body: unknown,
|
||||
|
||||
Reference in New Issue
Block a user