blocks-data-gateway-crud
Skill42 starsChanged 12 days agoMITView on GitHub
Implement create/read/update/delete against a SELISE Blocks project's runtime Data Gateway using the @seliseblocks/client SDK. Use data.collection(schemaName) for straightforward per-item CRUD, data.graphql() for joins or custom query shapes, and data.schemas.*/data.validations.* for schema/validation metadata. Shows how to wire CRUD into the React 18 + Vite + TanStack Query app that blocks new web scaffolds. Use whenever the user wants to read or write actual records through a Blocks Data schema from app code.
In this repository27 other files it gives its agents.
- AGENTS.mdAGENTS.md
- bootstrap/agents.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- blocks-bootstrapSkill
- blocks-captchaSkill
- blocks-data-gateway-configurationSkill
- blocks-data-storageSkill
- blocks-frontend-local-httpsSkill
- blocks-functionsSkill
- blocks-iam-access-controlSkill
- blocks-iam-accountSkill
- blocks-iam-mfaSkill
- blocks-iam-organizationsSkill
- blocks-iam-sso-oidc-configurationSkill
- blocks-iam-sso-oidc-implementationSkill
- blocks-iam-usersSkill
- blocks-localization-configurationSkill
- blocks-localization-implementationSkill
- blocks-mailSkill
- blocks-notificationSkill
- blocks-notifierSkill
- blocks-proxySkill
- blocks-release-deploymentSkill
- blocks-secretsSkill
- blocksSkill
- blocks-storage-configurationSkill
- blocks-workflowSkill
The file
We split this skill into the rules it gives an agent
9 rules. About 0 are common, 3 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 6
When invoking a project-scoped blocks command, either use the resolved account's saved selection or pass --project <tenantId> for that one …
- Distinctive: line 32
Import that existing instance from feature code.
- Distinctive: line 37
Never use the CLI's impersonation token or project token in browser code.
- Very specific: line 41
Use blocksClient.data.collection<T>(schemaName, options) for normal CRUD.
- Very specific: line 163
Use data.graphql({ query, variables, operationName }) when the collection helper is too small:
- Very specific: line 207
Use data.schemas.* when building dynamic UI:
- Very specific: line 215
Use data.validations.* to read field-level validation rules at runtime (e.g.
- Distinctive: line 231
If GraphQL says the field does not exist, the schema probably has not been created or reloaded.
- Very specific: line 232
Keep one blocksClient per app.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
When invoking a project-scoped blocks command, either use the resolved account's saved selection or pass --project <tenantId> for that one command without changing saved state. --project applies to CLI commands only, never SDK calls.
Blocks Data - Gateway CRUD
Once a schema exists and has been reloaded via the blocks-data-gateway-configuration skill, the Data Gateway exposes runtime records through GraphQL. This skill shows how to use the generated app's shared @seliseblocks/client instance for CRUD. Do not use raw fetch or curl against Blocks APIs from app code.
Prerequisite: a project selected via blocks use and an app scaffolded with blocks new web <name> .... If either is missing, run the blocks-bootstrap skill first.
Use the Existing Client
The scaffold creates one createBlocksClient() call in src/lib/blocks/client.ts:
import { createBlocksClient } from "@seliseblocks/client";
import { blocksConfig } from "./config";
import { getValidAccessToken } from "./auth";
export const blocksClient = createBlocksClient({
accessToken: () => getValidAccessToken(),
apiUrl: blocksConfig.apiUrl,
appDomain: blocksConfig.appDomain,
oidc: { clientId: blocksConfig.oidcClientId, scope: blocksConfig.oidcScope, url: blocksConfig.oidcUrl },
xBlocksKey: blocksConfig.xBlocksKey
});Import that existing instance from feature code. Do not create a second client.
xBlocksKeyis the project's public tenant key fromVITE_BLOCKS_X_BLOCKS_KEY.- The SDK does not add
ProjectKeyto runtime Data calls. - The HTTP client sends
credentials: "include"so hosted-login session cookies are included. - Never use the CLI's impersonation token or project token in browser code.
Preferred CRUD Helper
Use blocksClient.data.collection<T>(schemaName, options) for normal CRUD. Pass the schema name, not the collection name. For a schema named Product with collection Products, pass "Product".
const products = blocksClient.data.collection<Product>("Product", {
fields: ["name", "price", "status"]
});
products.list({ pageNo: 1, pageSize: 20 });
products.get(itemId);
products.create(payload);
products.update(itemId, payload);
products.delete(itemId);What the helper does:
listandgetcallquery getProducts($input: DynamicQueryInput).createcallsmutation insertProduct($input: ProductInsertInput!).updatecallsmutation updateProduct($filter: String, $input: ProductUpdateInput!).deletecallsmutation deleteProduct($filter: String, $input: ProductDeleteInput!).get,update, anddeleteaddress records byItemId.fieldscontrols the GraphQL selection for returned items;ItemIdis always selected.
list accepts paging plus optional GraphQL dynamic input values:
await products.list({
pageNo: 1,
pageSize: 20,
filter: { status: "Active" },
sort: { name: 1 }
});For search, only send filters that the schema/runtime actually supports. If unsure, start with simple paging and client-side filtering, or inspect the schema first.
Product Example
// src/features/products/productsApi.ts
import { blocksClient } from "../../lib/blocks/client";
export type Product = Record<string, unknown> & {
itemId?: string;
id?: string;
name?: string;
price?: number;
status?: string;
};
export type ProductInput = { name: string; price: number; status: string };
const products = blocksClient.data.collection<Product>("Product", {
fields: ["name", "price", "status"]
});
export async function listProducts({ page, pageSize }: { page: number; pageSize: number }) {
const response = await products.list({ pageNo: page, pageSize });
return normalizeProductList(response);
}
export function createProduct(input: ProductInput) {
return products.create(input);
}
export function updateProduct(product: Product) {
return products.update(productId(product), product);
}
export function deleteProduct(product: Product) {
return products.delete(productId(product));
}
function productId(product: Product): string {
const id = product.itemId ?? product.id;
if (!id) throw new Error("Product item id is missing.");
return String(id);
}
function normalizeProductList(response: unknown): { items: Product[]; totalCount: number } {
const record = response as {
data?: {
getProducts?: { items?: Product[]; totalCount?: number };
items?: Product[];
totalCount?: number;
};
items?: Product[];
totalCount?: number;
};
const gateway = record.data?.getProducts;
const items = gateway?.items ?? record.data?.items ?? record.items ?? [];
return { items, totalCount: gateway?.totalCount ?? record.data?.totalCount ?? record.totalCount ?? items.length };
}// src/features/products/useProducts.ts
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createProduct, deleteProduct, listProducts, updateProduct } from "./productsApi";
import type { Product, ProductInput } from "./productsApi";
export function useProductsQuery(page: number, pageSize: number) {
return useQuery({
queryKey: ["products", page, pageSize],
queryFn: () => listProducts({ page, pageSize })
});
}
export function useProductMutations() {
const queryClient = useQueryClient();
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["products"] });
return {
create: useMutation({ mutationFn: (input: ProductInput) => createProduct(input), onSuccess: invalidate }),
update: useMutation({ mutationFn: (product: Product) => updateProduct(product), onSuccess: invalidate }),
remove: useMutation({ mutationFn: (product: Product) => deleteProduct(product), onSuccess: invalidate })
};
}When to Use data.graphql()
Use data.graphql({ query, variables, operationName }) when the collection helper is too small:
- joins across schemas;
- custom nested selections;
- generated
where,order, orpagingarguments; - bulk operations such as
insertManyProduct.
Example:
const result = await blocksClient.data.graphql({
operationName: "getProducts",
query: `
query getProducts($input: DynamicQueryInput) {
getProducts(input: $input) {
items { ItemId name price status }
totalCount
}
}
`,
variables: { input: { pageNo: 1, pageSize: 20 } }
});Querying GeoJson fields
A GeoJson field is read and written as a plain GeoJSON geometry object ({ "type": "Point", "coordinates": [90.4125, 23.8103] } — longitude first). In where, it supports eq/neq (structural equality) and three geospatial operators:
near: { geometry: <Point>, maxDistanceMeters: 500, minDistanceMeters?: 100 }— within a distance of a point. Results are not distance-sorted.within: { geometry: <Polygon | MultiPolygon> }— entirely inside an area.intersects: { geometry: <any geometry> }— overlaps a geometry.
const result = await blocksClient.data.graphql({
query: `query ($where: StoreFilterInput) { getStores(where: $where) { items { name location } } }`,
variables: { where: { location: { near: { geometry: { type: "Point", coordinates: [90.4125, 23.8103] }, maxDistanceMeters: 2000 } } } }
});The where argument's type is <SchemaName>FilterInput; the query name is get${querySchema} (read it from blocks data schema get <id>, don't pluralize by hand). Geo operators need no setup: the field's 2dsphere index is created automatically (see blocks-data-gateway-configuration).
Schema Metadata
Use data.schemas.* when building dynamic UI:
data.schemas.list(options)ordata.schemas.get(schemaName)for discovery;data.schemas.infoByName(schemaName)for field-level metadata;data.schemas.info()for general schema info;data.schemas.aggregation(options)for summary views;data.schemas.getById(id)when you already have the schema id.
Use data.validations.* to read field-level validation rules at runtime (e.g. to drive client-side form validation from the same rules the backend enforces):
data.validations.list(options)for all rules, with optional schema/field/paging filters;data.validations.bySchemaId(schemaId)for every rule on one schema;data.validations.bySchemaAndField({ schemaId, fieldName })for one field's rule;data.validations.getById(id)when you already have the validation id.
Validation rules are authored and saved separately via blocks data validation save (see the blocks-data-gateway-configuration skill); this SDK namespace only reads them.
Gotchas
- Pass schema name, not collection name:
Product, notProducts. - Generated query names pluralize by naive concatenation -- literally appending
s, not English pluralization:getProducts, butCompany->getCompanys, notgetCompanies. Never guess a pluralized name; read it from the schema'squerySchemafield (viadata.schemas.get/getByNameorblocks data schema get <id>) and useget${querySchema}. - Generated mutation names stay singular:
insertProduct,updateProduct,deleteProduct. Bulk variants follow the same pattern but are not listed inmutationSchemas:insertManyProduct,updateManyProduct,deleteManyProduct. - Dynamic item selections use schema field names such as
ItemId,name,price. - Mutation response fields are lower camel case:
acknowledged,itemId,message,totalImpactedData. - If GraphQL says the field does not exist, the schema probably has not been created or reloaded.
- Keep one
blocksClientper app.
Prompt Routing
Wire create/read/update/delete for Product into my React app -> this skill
Build a paged table of Orders in the dashboard -> this skill
I need a GraphQL query that joins Orders with Customer details -> this skill, data.graphql
Build a dynamic form from my schema's field metadata -> this skill, data.schemas.infoByName
Create a Product schema with title/price and reload it -> blocks-data-gateway-configuration
Upload a PDF and get a download link -> blocks-data-storageDoes it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

