Archiviazione

In questa pagina

I plugin sandboxed possono memorizzare i propri dati nelle collezioni di documenti. Dichiari le collezioni e gli indici nel descrittore del plugin, ed EmDash crea automaticamente lo schema — nessuna migrazione da scrivere.

Questa pagina copre i plugin sandboxed (formato standard). L’API delle collezioni è identica per i plugin nativi; l’unica differenza è che i plugin nativi dichiarano storage direttamente all’interno di definePlugin() piuttosto che su un descrittore separato.

Dichiarare l’archiviazione nel descrittore

Per i plugin sandboxed, storage si trova nel descrittore — il file importato da astro.config.mjs, non l’entry sandbox. Le dichiarazioni di archiviazione devono essere visibili al momento della build in modo che il bridge sandbox sappia quali collezioni il plugin può toccare.

import type { PluginDescriptor } from "emdash";

export function formsPlugin(): PluginDescriptor {
	return {
		id: "forms",
		version: "1.0.0",
		format: "standard",
		entrypoint: "@my-org/plugin-forms/sandbox",

		storage: {
			submissions: {
				indexes: [
					"formId",
					"status",
					"createdAt",
					["formId", "createdAt"],
					["status", "createdAt"],
				],
			},
			forms: {
				indexes: ["slug"],
			},
		},
	};
}

Ogni chiave in storage è un nome di collezione. L’array indexes elenca i campi che possono essere interrogati in modo efficiente — indici a campo singolo come stringhe, indici compositi come array di stringhe.

Utilizzare l’archiviazione nell’entry sandbox

All’interno dell’entry sandbox, accedi alle collezioni tramite ctx.storage. La forma rispecchia ciò che è stato dichiarato nel descrittore:

import { definePlugin } from "emdash";
import type { PluginContext } from "emdash";

export default definePlugin({
	hooks: {
		"content:afterSave": async (event, ctx: PluginContext) => {
			const { submissions } = ctx.storage;

			await submissions.put("sub_123", {
				formId: "contact",
				email: "[email protected]",
				status: "pending",
				createdAt: new Date().toISOString(),
			});

			const item = await submissions.get("sub_123");
			ctx.log.info("Stored submission", { id: item?.formId });
		},
	},
});

L’accesso a una collezione che non è stata dichiarata nel descrittore genera un’eccezione — il bridge applica questo a livello di runtime.

API della collezione

interface StorageCollection<T = unknown> {
	// CRUD di base
	get(id: string): Promise<T | null>;
	put(id: string, data: T): Promise<void>;
	delete(id: string): Promise<boolean>;
	exists(id: string): Promise<boolean>;

	// Operazioni batch
	getMany(ids: string[]): Promise<Map<string, T>>;
	putMany(items: Array<{ id: string; data: T }>): Promise<void>;
	deleteMany(ids: string[]): Promise<number>;

	// Query (solo campi indicizzati)
	query(options?: QueryOptions): Promise<PaginatedResult<{ id: string; data: T }>>;
	count(where?: WhereClause): Promise<number>;
}

Query

query() restituisce risultati paginati filtrati per campi indicizzati:

const result = await ctx.storage.submissions.query({
	where: {
		formId: "contact",
		status: "pending",
	},
	orderBy: { createdAt: "desc" },
	limit: 20,
});

// result.items   — Array<{ id, data }>
// result.cursor  — cursore di paginazione (se esistono più risultati)
// result.hasMore — boolean

Opzioni di query

interface QueryOptions {
	where?: WhereClause;
	orderBy?: Record<string, "asc" | "desc">;
	limit?: number;     // predefinito 50, max 1000
	cursor?: string;    // per la paginazione
}

Operatori della clausola Where

Filtra per campi indicizzati usando questi operatori:

Corrispondenza esatta

where: {
	status: "pending",     // corrispondenza esatta di stringa
	count: 5,              // corrispondenza esatta di numero
	archived: false,       // corrispondenza esatta di booleano
}

Intervallo

where: {
	createdAt: { gte: "2024-01-01" },
	score: { gt: 50, lte: 100 },
}
// Disponibili: gt, gte, lt, lte

In lista

where: {
	status: { in: ["pending", "approved"] },
}

Inizia con

where: {
	slug: { startsWith: "blog-" },
}

Ordinamento

orderBy: { createdAt: "desc" }   // più recenti per primi
orderBy: { score: "asc" }        // più bassi per primi

Paginazione

Esaurisci un cursore per attraversare tutti gli elementi corrispondenti:

async function getAllSubmissions(ctx: PluginContext) {
	const all: Array<{ id: string; data: unknown }> = [];
	let cursor: string | undefined;

	do {
		const result = await ctx.storage.submissions.query({
			orderBy: { createdAt: "desc" },
			limit: 100,
			cursor,
		});
		all.push(...result.items);
		cursor = result.cursor;
	} while (cursor);

	return all;
}

Conteggio

const total = await ctx.storage.submissions.count();

const pending = await ctx.storage.submissions.count({
	status: "pending",
});

Operazioni batch

const items = await ctx.storage.submissions.getMany(["sub_1", "sub_2", "sub_3"]);
// Restituisce Map<string, T>

await ctx.storage.submissions.putMany([
	{ id: "sub_1", data: { formId: "contact", status: "new" } },
	{ id: "sub_2", data: { formId: "contact", status: "new" } },
]);

const deletedCount = await ctx.storage.submissions.deleteMany(["sub_1", "sub_2"]);

Design degli indici

Scegli gli indici in base ai pattern di query effettivi:

Pattern di queryIndice necessario
Filtra per formId"formId"
Filtra per formId, ordina per createdAt["formId", "createdAt"]
Ordina solo per createdAt"createdAt"
Filtra per status e formId"status" e "formId" (separati)

Gli indici compositi supportano query che filtrano sul primo campo e opzionalmente ordinano per il secondo:

// Con indice ["formId", "createdAt"]:
query({ where: { formId: "contact" }, orderBy: { createdAt: "desc" } });  // usa l'indice
query({ where: { formId: "contact" } });                                  // usa l'indice (solo filtro)
query({ where: { createdAt: { gte: "2024-01-01" } } });                   // NON usa questo composito — il filtro inizia dal campo sbagliato

Sicurezza dei tipi

Esegui il cast dell’accesso alla collezione per IntelliSense sulle forme degli elementi:

import type { StorageCollection, PluginContext } from "emdash";

interface Submission {
	formId: string;
	email: string;
	data: Record<string, unknown>;
	status: "pending" | "approved" | "spam";
	createdAt: string;
}

export default definePlugin({
	hooks: {
		"content:afterSave": async (event, ctx: PluginContext) => {
			const submissions = ctx.storage.submissions as StorageCollection<Submission>;

			await submissions.put(`sub_${Date.now()}`, {
				formId: "contact",
				email: "[email protected]",
				data: { message: "Hello" },
				status: "pending",
				createdAt: new Date().toISOString(),
			});
		},
	},
});

Storage vs content vs KV

Scegli il meccanismo giusto per ogni tipo di dato:

Caso d’usoArchiviazione
Dati operativi del plugin (log, invii, cache)ctx.storage
Impostazioni configurabili dall’utentectx.kv con prefisso settings:
Stato interno del pluginctx.kv con prefisso state:
Contenuto modificabile nell’interfaccia adminCollezioni del sito (non archiviazione plugin)

Se gli editor del sito devono visualizzare o modificare i dati nell’interfaccia admin attraverso l’editor di contenuti regolare, crea invece una collezione del sito.

Dettagli di implementazione

L’archiviazione del plugin utilizza una singola tabella con namespace:

CREATE TABLE _plugin_storage (
	plugin_id TEXT NOT NULL,
	collection TEXT NOT NULL,
	id TEXT NOT NULL,
	data JSON NOT NULL,
	created_at TEXT,
	updated_at TEXT,
	PRIMARY KEY (plugin_id, collection, id)
);

EmDash crea indici di espressione per i campi dichiarati:

CREATE INDEX idx_forms_submissions_formId
	ON _plugin_storage(json_extract(data, '$.formId'))
	WHERE plugin_id = 'forms' AND collection = 'submissions';

Questo design ti offre nessuna migrazione, portabilità tra SQLite/libSQL/D1, isolamento a livello di plugin e query parametrizzate su ogni percorso.

Aggiunta di indici

Quando aggiungi un indice in un aggiornamento del plugin, EmDash lo crea automaticamente al prossimo avvio. L’aggiunta di un indice è sicura e non richiede migrazione dei dati. Quando rimuovi un indice, EmDash lo elimina — e le query su quel campo iniziano a fallire con un errore di validazione, che è il segnale previsto.