diff --git a/.changeset/18915-published-readme-examples-compile.md b/.changeset/18915-published-readme-examples-compile.md new file mode 100644 index 00000000000..21bc2b3ec76 --- /dev/null +++ b/.changeset/18915-published-readme-examples-compile.md @@ -0,0 +1,35 @@ +--- +"@objectstack/cli": patch +"@objectstack/client": patch +"@objectstack/client-react": patch +"@objectstack/driver-memory": patch +"@objectstack/driver-mongodb": patch +"@objectstack/driver-turso": patch +"@objectstack/mcp": patch +"@objectstack/observability": patch +"@objectstack/plugin-auth": patch +"@objectstack/rest": patch +"@objectstack/runtime": patch +"@objectstack/service-cache": patch +"@objectstack/service-i18n": patch +"@objectstack/service-job": patch +"@objectstack/service-package": patch +"@objectstack/service-queue": patch +"@objectstack/service-realtime": patch +"@objectstack/service-storage": patch +"@objectstack/spec": patch +"@objectstack/types": patch +--- + +The TypeScript examples in these packages' **published** `README.md` now compile against the package they document — 43 of the 44 blocks the `measure-markdown-ts-blocks` census reported as syntactically valid and wrong, in documents that ship inside the npm tarball. + +`README.md` is listed in every one of these packages' `files[]`, so these bytes are the artefact a consumer — or a consumer's AI — reads and copies. What the census counted was not style: the examples named options the packages no longer accept, chained a method that returns a promise, and implemented interfaces they never imported. + +The corrections, by class: + +- **Legacy option vocabulary.** `@objectstack/client-react`'s hooks take `fields` / `orderBy` / `limit` / `where`, not `select` / `sort` / `top` / `filters`, and `PaginatedResult` carries `records`, not `value`. `@objectstack/service-job` takes `timeoutMs`, `@objectstack/service-queue` takes `maxAttempts`, and `IDataEngine.find` takes `where`. +- **Async registration used synchronously.** `ObjectKernel.use()` returns `Promise`, so `kernel.use(a).use(b)` does not chain; the examples now `await` each registration. `ObjectKernelConfig` has no `plugins` member. +- **Interfaces implemented but never imported.** Several plugin examples wrote `implements Plugin` with no import, which bound to the DOM's `Plugin`; they now import `Plugin` / `PluginContext` and declare the required `init`. `PluginContext.getService()` has no default type argument, so the examples that read a service now name its contract. +- **Removed or never-existing API.** `@objectstack/driver-memory`'s default export is a legacy `onEnable` object that `kernel.use()` refuses — the quick start now registers through `DriverPlugin`; its persistence adapters take an options bag under `persistence.adapter`. `defineStack` has no `driver` key. `@objectstack/rest`'s `RestServer` takes the host `IHttpServer` first and `registerRoutes()` takes no arguments; `RouteManager` is constructed on a server. `@objectstack/spec`'s `ObjectSchema.parse()` returns the value — the `{ success, data }` envelope is `safeParse`'s. `useMutation` has no `onMutate` / mutation context. + +No runtime code changed and no gate was added (#18715 ruling F). One block is deliberately left: `@objectstack/knowledge-ragflow`'s README writes `source.options.datasetId`, which is what the shipped adapter reads and what `KnowledgeSourceSchema` does not declare — correcting the document either way would contradict one of the two, so the conflict is reported rather than papered over. diff --git a/packages/cli/README.md b/packages/cli/README.md index 82879647157..c6eb92396c1 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -131,7 +131,7 @@ The CLI looks for `objectstack.config.ts` (or `.js`, `.mjs`) in the current dire ```typescript import { defineStack } from '@objectstack/spec'; -import * as objects from './src/objects'; +import { project, task } from './src/objects'; export default defineStack({ manifest: { @@ -141,7 +141,7 @@ export default defineStack({ type: 'app', name: 'My App', }, - objects: Object.values(objects), + objects: [project, task], }); ``` diff --git a/packages/client-react/README.md b/packages/client-react/README.md index 2c6863e4d59..69d368ddb66 100644 --- a/packages/client-react/README.md +++ b/packages/client-react/README.md @@ -51,9 +51,9 @@ import { useQuery } from '@objectstack/client-react'; function TaskList() { const { data, isLoading, error, refetch } = useQuery('todo_task', { - select: ['id', 'subject', 'priority'], - sort: ['-created_at'], - top: 20 + fields: ['id', 'subject', 'priority'], + orderBy: ['-created_at'], + limit: 20 }); if (isLoading) return
Loading...
; @@ -61,7 +61,7 @@ function TaskList() { return (
- {data?.value.map(task => ( + {data?.records.map(task => (
{task.subject}
))} @@ -73,6 +73,7 @@ function TaskList() { #### Mutate Data ```tsx +import type { FormEvent } from 'react'; import { useMutation } from '@objectstack/client-react'; function CreateTaskForm() { @@ -82,7 +83,7 @@ function CreateTaskForm() { } }); - const handleSubmit = (e) => { + const handleSubmit = (e: FormEvent) => { e.preventDefault(); mutate({ subject: 'New Task', @@ -119,12 +120,12 @@ function PaginatedTaskList() { hasPreviousPage } = usePagination('todo_task', { pageSize: 10, - sort: ['-created_at'] + orderBy: ['-created_at'] }); return (
- {data?.value.map(task => ( + {data?.records.map(task => (
{task.subject}
))}
@@ -155,7 +156,7 @@ function InfiniteTaskList() { isFetchingNextPage } = useInfiniteQuery('todo_task', { pageSize: 20, - sort: ['-created_at'] + orderBy: ['-created_at'] }); return ( @@ -180,7 +181,7 @@ function InfiniteTaskList() { ```tsx import { useObject } from '@objectstack/client-react'; -function ObjectSchemaViewer({ objectName }) { +function ObjectSchemaViewer({ objectName }: { objectName: string }) { const { data: schema, isLoading } = useObject(objectName); if (isLoading) return
Loading schema...
; @@ -199,7 +200,7 @@ function ObjectSchemaViewer({ objectName }) { ```tsx import { useView } from '@objectstack/client-react'; -function ViewConfiguration({ objectName }) { +function ViewConfiguration({ objectName }: { objectName: string }) { const { data: view, isLoading } = useView(objectName, 'list'); if (isLoading) return
Loading view...
; @@ -218,7 +219,7 @@ function ViewConfiguration({ objectName }) { ```tsx import { useFields } from '@objectstack/client-react'; -function FieldList({ objectName }) { +function FieldList({ objectName }: { objectName: string }) { const { data: fields, isLoading } = useFields(objectName); if (isLoading) return
Loading fields...
; @@ -269,7 +270,7 @@ interface Task { } const { data } = useQuery('todo_task'); -// data.value is typed as Task[] +// data.records is typed as Task[] const { mutate } = useMutation>('todo_task', 'create'); // mutate expects Partial @@ -280,23 +281,26 @@ const { mutate } = useMutation>('todo_task', 'create'); ### Master-Detail View ```tsx +import { useState } from 'react'; +import { useQuery } from '@objectstack/client-react'; + function TaskList() { const [selectedId, setSelectedId] = useState(null); - + const { data: tasks } = useQuery('todo_task', { - select: ['id', 'subject'], - sort: ['-created_at'] + fields: ['id', 'subject'], + orderBy: ['-created_at'] }); - + const { data: selectedTask } = useQuery('todo_task', { - filters: ['id', '=', selectedId], + where: { id: selectedId }, enabled: !!selectedId // Only fetch when ID is selected }); - + return (
- - + +
); } @@ -305,26 +309,31 @@ function TaskList() { ### Optimistic Updates ```tsx -function TaskToggle({ taskId, completed }) { +import { useState } from 'react'; +import { useMutation } from '@objectstack/client-react'; + +function TaskToggle({ taskId, completed }: { taskId: string; completed: boolean }) { + // `useMutation` has no mutation-context hook, so the optimistic value is held + // locally and rolled back from `onError`. + const [checked, setChecked] = useState(completed); + const { mutate } = useMutation('todo_task', 'update', { - onMutate: async (variables) => { - // Optimistically update UI - return { previousValue: completed }; - }, - onError: (error, variables, context) => { - // Revert on error - console.error('Update failed, reverting', context.previousValue); - }, - onSuccess: () => { - // Refetch to ensure data consistency - queryClient.invalidateQueries(['todo_task']); + onError: (error: Error) => { + console.error('Update failed, reverting', error); + setChecked(completed); } }); - + return ( - mutate({ id: taskId, is_completed: e.target.checked })} + { + const next = e.target.checked; + setChecked(next); + // The `update` operation takes `{ id, data }`. + mutate({ id: taskId, data: { is_completed: next } }); + }} /> ); } @@ -333,22 +342,24 @@ function TaskToggle({ taskId, completed }) { ### Dependent Queries ```tsx -function ProjectTasks({ projectId }) { +import { useQuery } from '@objectstack/client-react'; + +function ProjectTasks({ projectId }: { projectId: string }) { // First, get project details const { data: project } = useQuery('project', { - filters: ['id', '=', projectId] + where: { id: projectId } }); - + // Then, get tasks for this project const { data: tasks } = useQuery('todo_task', { - filters: ['project_id', '=', projectId], + where: { project_id: projectId }, enabled: !!project // Only fetch when project is loaded }); - + return (
-

{project?.value?.[0]?.name}

- +

{project?.records?.[0]?.name}

+
); } @@ -357,14 +368,15 @@ function ProjectTasks({ projectId }) { ### Search with Debounce ```tsx -import { useDeferredValue } from 'react'; +import { useDeferredValue, useState } from 'react'; +import { useQuery } from '@objectstack/client-react'; function TaskSearch() { const [searchTerm, setSearchTerm] = useState(''); const deferredSearch = useDeferredValue(searchTerm); const { data, isLoading } = useQuery('todo_task', { - filters: ['subject', 'contains', deferredSearch], + where: { subject: { $contains: deferredSearch } }, enabled: deferredSearch.length >= 3 // Only search with 3+ chars }); @@ -377,7 +389,7 @@ function TaskSearch() { placeholder="Search tasks..." /> {isLoading && } - +
); } diff --git a/packages/client/README.md b/packages/client/README.md index 8b573e03ac8..8e26ff53638 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -46,8 +46,9 @@ async function main() { // 2. Connect (Fetches system capabilities) await client.connect(); - // 3. Metadata Access - const todoSchema = await client.meta.getItem('object', 'todo_task'); + // 3. Metadata Access — the document is carried under `item` + const { item } = await client.meta.getItem('object', 'todo_task'); + const todoSchema = item as { fields: Record }; console.log('Fields:', todoSchema.fields); // Save Metadata (New Feature) @@ -143,9 +144,12 @@ Batch operations support the following options: The client provides standardized error handling with machine-readable error codes: ```typescript +import type { StandardError } from '@objectstack/client'; + try { await client.data.create('todo_task', { subject: '' }); -} catch (error) { +} catch (caught) { + const error = caught as Error & Partial; console.error('Error code:', error.code); // e.g., 'validation_error' console.error('Category:', error.category); // e.g., 'validation' console.error('HTTP status:', error.httpStatus); // e.g., 400 @@ -288,7 +292,7 @@ const cubes = await client.analytics.meta('sales'); console.log(cubes[0].name); // Automation -const run = await client.automation.trigger('send_welcome_email', { userId }); +const run = await client.automation.trigger('send_welcome_email', { userId: 'usr_123' }); console.log(run.status); // File Storage diff --git a/packages/drivers/driver-memory/README.md b/packages/drivers/driver-memory/README.md index 552c1e93caf..3d88c71ef54 100644 --- a/packages/drivers/driver-memory/README.md +++ b/packages/drivers/driver-memory/README.md @@ -19,10 +19,11 @@ pnpm add @objectstack/driver-memory ```typescript import { ObjectKernel } from '@objectstack/core'; -import memoryPlugin from '@objectstack/driver-memory'; +import { DriverPlugin } from '@objectstack/runtime'; +import { InMemoryDriver } from '@objectstack/driver-memory'; const kernel = new ObjectKernel(); -kernel.use(memoryPlugin); // default plugin +await kernel.use(new DriverPlugin(new InMemoryDriver(), 'memory')); await kernel.bootstrap(); ``` @@ -41,7 +42,7 @@ await driver.connect(); import { InMemoryDriver, FileSystemPersistenceAdapter } from '@objectstack/driver-memory'; const driver = new InMemoryDriver({ - persistence: new FileSystemPersistenceAdapter('./data/snapshot.json'), + persistence: { adapter: new FileSystemPersistenceAdapter({ path: './data/snapshot.json' }) }, }); await driver.connect(); ``` @@ -52,7 +53,7 @@ await driver.connect(); import { InMemoryDriver, LocalStoragePersistenceAdapter } from '@objectstack/driver-memory'; const driver = new InMemoryDriver({ - persistence: new LocalStoragePersistenceAdapter('objectstack:dev'), + persistence: { adapter: new LocalStoragePersistenceAdapter({ key: 'objectstack:dev' }) }, }); ``` @@ -60,7 +61,7 @@ const driver = new InMemoryDriver({ | Export | Kind | Description | |:---|:---|:---| -| `default` | kernel plugin | Drop-in plugin. | +| `default` | legacy plugin object | Legacy `onEnable` shape — not a kernel `Plugin`; register through `DriverPlugin`. | | `InMemoryDriver` | class | Driver instance for direct use. | | `InMemoryStrategy` | class | Query execution strategy used by ObjectQL. | | `FileSystemPersistenceAdapter` | class | Node-only persistence. | diff --git a/packages/drivers/driver-mongodb/README.md b/packages/drivers/driver-mongodb/README.md index 41c5e61b089..01e506778aa 100644 --- a/packages/drivers/driver-mongodb/README.md +++ b/packages/drivers/driver-mongodb/README.md @@ -36,16 +36,19 @@ explicit override (recognised values: `mongodb`, `mongo`). ```typescript import { defineStack } from '@objectstack/spec'; +import { DriverPlugin } from '@objectstack/runtime'; import { MongoDBDriver } from '@objectstack/driver-mongodb'; export default defineStack({ - driver: new MongoDBDriver({ - url: 'mongodb://localhost:27017/myapp', - database: 'myapp', // Optional: overrides URI database - maxPoolSize: 10, // Optional: connection pool size (default: 10) - minPoolSize: 1, // Optional: minimum pool (default: 1) - connectTimeoutMS: 10000, // Optional: connection timeout - }), + plugins: [ + new DriverPlugin(new MongoDBDriver({ + url: 'mongodb://localhost:27017/myapp', + database: 'myapp', // Optional: overrides URI database + maxPoolSize: 10, // Optional: connection pool size (default: 10) + minPoolSize: 1, // Optional: minimum pool (default: 1) + connectTimeoutMS: 10000, // Optional: connection timeout + }), 'mongodb'), + ], }); ``` diff --git a/packages/drivers/driver-turso/README.md b/packages/drivers/driver-turso/README.md index cb867e7b68a..c4fd445c240 100644 --- a/packages/drivers/driver-turso/README.md +++ b/packages/drivers/driver-turso/README.md @@ -171,7 +171,8 @@ the bound into your own client if you need both: const client = createClient({ url: 'libsql://my-db.turso.io', authToken: process.env.TURSO_AUTH_TOKEN, - fetch: (input, init) => fetch(input, { ...init, signal: AbortSignal.timeout(30_000) }), + fetch: (input: RequestInfo | URL, init?: RequestInit) => + fetch(input, { ...init, signal: AbortSignal.timeout(30_000) }), }); ``` diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 6a9faa85da6..925b1281833 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -505,7 +505,7 @@ View MCP messages in client: import { defineStack } from '@objectstack/spec'; import { MCPServerPlugin } from '@objectstack/mcp'; -import * as objects from './src/objects/index.js'; +import { account, contact, opportunity } from './src/objects/index.js'; import { allActions } from './src/actions/index.js'; export default defineStack({ @@ -517,7 +517,7 @@ export default defineStack({ name: 'CRM Assistant', engines: { protocol: '^17' }, }, - objects: Object.values(objects), + objects: [account, contact, opportunity], // Your actions become MCP tools — the plugin bridges them at start. actions: allActions, plugins: [new MCPServerPlugin({ name: 'crm-assistant' })], diff --git a/packages/observability/README.md b/packages/observability/README.md index 9d8bf8f2596..d28de022702 100644 --- a/packages/observability/README.md +++ b/packages/observability/README.md @@ -29,20 +29,29 @@ framework runtime and services. ## Wiring +Pick one registry per deployment. + +Self-hosted (any K8s with an OpenTelemetry Collector): + ```ts -// Self-hosted (any K8s with an OpenTelemetry Collector): const metrics = new OtlpHttpMetricsRegistry({ endpoint: 'http://otel-collector:4318', resource: { 'service.name': 'objectos', 'deployment.environment': 'prod' }, }); +``` + +Cloudflare Workers are handled in `apps/cloud` — see that repo's exporter +(`new AnalyticsEngineRegistry(env.AE)`). -// Cloudflare Workers (handled in apps/cloud — see that repo's exporter): -// const metrics = new AnalyticsEngineRegistry(env.AE); +Local dev: -// Local dev: +```ts const metrics = new ConsoleMetricsRegistry(); +``` -// Tests: +Tests: + +```ts const metrics = new InMemoryMetricsRegistry(); expect(metrics.totalCounter('http_requests_total', { status: '500' })).toBe(0); ``` diff --git a/packages/plugins/plugin-auth/README.md b/packages/plugins/plugin-auth/README.md index a8891b5e971..df3f832daab 100644 --- a/packages/plugins/plugin-auth/README.md +++ b/packages/plugins/plugin-auth/README.md @@ -63,22 +63,20 @@ import { ObjectQL } from '@objectstack/objectql'; // Initialize ObjectQL as the data engine const dataEngine = new ObjectQL(); -const kernel = new ObjectKernel({ - plugins: [ - new AuthPlugin({ - secret: process.env.AUTH_SECRET, - baseUrl: 'http://localhost:3000', - // ObjectQL will be automatically injected by the kernel - providers: [ - { - id: 'google', - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - } - ] - }) +const kernel = new ObjectKernel(); + +await kernel.use(new AuthPlugin({ + secret: process.env.AUTH_SECRET, + baseUrl: 'http://localhost:3000', + // ObjectQL will be automatically injected by the kernel + providers: [ + { + id: 'google', + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + } ] -}); +})); ``` **Note:** The `databaseUrl` parameter is no longer used. The plugin now uses ObjectQL's IDataEngine interface, which is provided by the kernel's `data` service. This allows the plugin to work with any ObjectQL-compatible driver (memory, SQL, NoSQL, etc.) without requiring a specific ORM. @@ -181,8 +179,10 @@ This package provides authentication services powered by better-auth. Current im The plugin uses a **direct forwarding** approach: ```typescript +import type { Context } from 'hono'; + // All requests under /api/v1/auth/* are forwarded to better-auth -rawApp.all('/api/v1/auth/*', async (c) => { +rawApp.all('/api/v1/auth/*', async (c: Context) => { const request = c.req.raw; // Web standard Request const response = await authManager.handleRequest(request); return response; // Web standard Response diff --git a/packages/rest/README.md b/packages/rest/README.md index a8d48b3aa0f..067ddb798c7 100644 --- a/packages/rest/README.md +++ b/packages/rest/README.md @@ -23,10 +23,12 @@ import { createRestApiPlugin } from '@objectstack/rest'; const kernel = new ObjectKernel(); -kernel.use(createRestApiPlugin({ +await kernel.use(createRestApiPlugin({ api: { - version: 'v1', - basePath: '/api', + api: { + version: 'v1', + basePath: '/api', + }, }, })); @@ -38,8 +40,10 @@ await kernel.bootstrap(); ```typescript import { RestServer } from '@objectstack/rest'; -const server = new RestServer(protocol, config); -server.registerRoutes(dispatcher); +const server = new RestServer(httpServer, protocol, { + api: { version: 'v1', basePath: '/api' }, +}); +server.registerRoutes(); ``` ### Custom routes via `RouteManager` @@ -47,8 +51,14 @@ server.registerRoutes(dispatcher); ```typescript import { RouteManager } from '@objectstack/rest'; -const routes = new RouteManager(); -routes.register({ method: 'GET', path: '/custom', handler }); +const routes = new RouteManager(httpServer); +routes.register({ + method: 'GET', + path: '/custom', + handler: async (req, res) => { + res.json({ ok: true }); + }, +}); ``` ## Generated endpoints diff --git a/packages/runtime/README.md b/packages/runtime/README.md index ab19ff54598..4303b547332 100644 --- a/packages/runtime/README.md +++ b/packages/runtime/README.md @@ -40,15 +40,14 @@ import { InMemoryDriver } from '@objectstack/driver-memory'; const kernel = new ObjectKernel(); -kernel - // Register ObjectQL engine - .use(new ObjectQLPlugin()) - - // Add database driver - .use(new DriverPlugin(new InMemoryDriver(), 'memory')) - - // Add your app configurations - // .use(new AppPlugin(appConfig)); +// Register ObjectQL engine +await kernel.use(new ObjectQLPlugin()); + +// Add database driver +await kernel.use(new DriverPlugin(new InMemoryDriver(), 'memory')); + +// Add your app configurations +// await kernel.use(new AppPlugin(appConfig)); await kernel.bootstrap(); ``` @@ -60,6 +59,7 @@ If you have a separate ObjectQL implementation or need custom configuration: ```typescript import { ObjectKernel, DriverPlugin } from '@objectstack/runtime'; import { ObjectQLPlugin, ObjectQL } from '@objectstack/objectql'; +import { InMemoryDriver } from '@objectstack/driver-memory'; // Create custom ObjectQL instance const customQL = new ObjectQL({ @@ -74,12 +74,11 @@ customQL.registerHook('beforeInsert', async (ctx) => { const kernel = new ObjectKernel(); -kernel - // Use your custom ObjectQL instance - .use(new ObjectQLPlugin(customQL)) - - // Add driver - .use(new DriverPlugin(new InMemoryDriver(), 'memory')); +// Use your custom ObjectQL instance +await kernel.use(new ObjectQLPlugin(customQL)); + +// Add driver +await kernel.use(new DriverPlugin(new InMemoryDriver(), 'memory')); await kernel.bootstrap(); @@ -137,12 +136,13 @@ new AppPlugin(appConfig) Abstract interface for HTTP server capabilities. Allows plugins to work with any HTTP framework (e.g. Hono) without tight coupling. ```typescript -import { IHttpServer, IHttpRequest, IHttpResponse } from '@objectstack/runtime'; +import type { Plugin, PluginContext } from '@objectstack/core'; +import type { IHttpServer } from '@objectstack/runtime'; // In your HTTP server plugin class MyHttpServerPlugin implements Plugin { name = 'http-server'; - + async init(ctx: PluginContext) { const server: IHttpServer = createMyServer(); // Hono, or any framework via IHttpServer ctx.registerService('http-server', server); @@ -153,10 +153,13 @@ class MyHttpServerPlugin implements Plugin { class MyApiPlugin implements Plugin { name = 'api'; dependencies = ['http-server']; - + + // `init` is required on every plugin, even when it registers nothing. + async init(_ctx: PluginContext) {} + async start(ctx: PluginContext) { const server = ctx.getService('http-server'); - + // Register routes - works with any HTTP framework server.get('/api/users', async (req, res) => { res.json({ users: [] }); @@ -180,12 +183,13 @@ class MyApiPlugin implements Plugin { Abstract interface for data persistence. Allows plugins to work with any data layer (ObjectQL, Prisma, TypeORM, etc.) without tight coupling. ```typescript -import { IDataEngine } from '@objectstack/runtime'; +import type { Plugin, PluginContext } from '@objectstack/core'; +import type { IDataEngine } from '@objectstack/runtime'; // In your data plugin class MyDataPlugin implements Plugin { name = 'data'; - + async init(ctx: PluginContext) { const engine: IDataEngine = createMyDataEngine(); // ObjectQL, Prisma, etc. ctx.registerService('data-engine', engine); @@ -196,13 +200,16 @@ class MyDataPlugin implements Plugin { class MyBusinessPlugin implements Plugin { name = 'business'; dependencies = ['data']; - + + // `init` is required on every plugin, even when it registers nothing. + async init(_ctx: PluginContext) {} + async start(ctx: PluginContext) { const engine = ctx.getService('data-engine'); // CRUD operations - works with any data layer const user = await engine.insert('user', { name: 'John' }); - const users = await engine.find('user', { filter: { active: true } }); + const users = await engine.find('user', { where: { active: true } }); await engine.update('user', { id: user.id, name: 'Jane' }); await engine.delete('user', { where: { id: user.id } }); } @@ -416,8 +423,11 @@ export class ApiPlugin implements Plugin { } // Usage -kernel.use(new ApiPlugin({ - apiKey: process.env.API_KEY, +const apiKey = process.env.API_KEY; +if (!apiKey) throw new Error('API_KEY is not set'); + +await kernel.use(new ApiPlugin({ + apiKey, endpoint: 'https://api.example.com', timeout: 10000 })); @@ -466,15 +476,18 @@ export class ConnectionPoolPlugin implements Plugin { ### Middleware Pattern ```typescript -import { Plugin, PluginContext } from '@objectstack/core'; +import type { IHttpServer, Plugin, PluginContext } from '@objectstack/core'; export class LoggingMiddleware implements Plugin { name = 'logging-middleware'; dependencies = ['http-server']; - + + // `init` is required on every plugin, even when it registers nothing. + async init(_ctx: PluginContext) {} + async start(ctx: PluginContext) { - const server = ctx.getService('http-server'); - + const server = ctx.getService('http-server'); + // Register middleware server.use(async (req, res, next) => { const start = Date.now(); @@ -500,24 +513,27 @@ export class LoggingMiddleware implements Plugin { ### Lazy Loading Pattern ```typescript -import { Plugin, PluginContext } from '@objectstack/core'; +import type { Plugin, PluginContext } from '@objectstack/core'; export class HeavyServicePlugin implements Plugin { name = 'heavy-service'; - private instance: any = null; - + async init(ctx: PluginContext) { - // Register factory instead of instance + // Register factory instead of instance. The cached instance is a closure + // variable: inside the object literal `this` is the literal itself, not + // the plugin. + let instance: unknown = null; + const factory = { - async getInstance() { - if (!this.instance) { + async getInstance(): Promise { + if (!instance) { ctx.logger.info('Lazy loading heavy service...'); - this.instance = await loadHeavyService(); + instance = await loadHeavyService(); } - return this.instance; + return instance; } }; - + ctx.registerService('heavy-service', factory); } } @@ -530,15 +546,18 @@ const service = await factory.getInstance(); // Loaded only when needed ### Health Check Pattern ```typescript -import { Plugin, PluginContext } from '@objectstack/core'; +import type { IHttpServer, Plugin, PluginContext } from '@objectstack/core'; export class HealthCheckPlugin implements Plugin { name = 'health-check'; dependencies = ['http-server', 'database', 'cache']; - + + // `init` is required on every plugin, even when it registers nothing. + async init(_ctx: PluginContext) {} + async start(ctx: PluginContext) { - const server = ctx.getService('http-server'); - + const server = ctx.getService('http-server'); + server.get('/health', async (req, res) => { const checks = await Promise.all([ this.checkDatabase(ctx), @@ -557,21 +576,21 @@ export class HealthCheckPlugin implements Plugin { private async checkDatabase(ctx: PluginContext) { try { - const db = ctx.getService('database'); + const db = ctx.getService<{ ping(): Promise }>('database'); await db.ping(); return { name: 'database', healthy: true }; } catch (error) { - return { name: 'database', healthy: false, error: error.message }; + return { name: 'database', healthy: false, error: (error as Error).message }; } } - + private async checkCache(ctx: PluginContext) { try { - const cache = ctx.getService('cache'); + const cache = ctx.getService<{ ping(): Promise }>('cache'); await cache.ping(); return { name: 'cache', healthy: true }; } catch (error) { - return { name: 'cache', healthy: false, error: error.message }; + return { name: 'cache', healthy: false, error: (error as Error).message }; } } diff --git a/packages/services/service-cache/README.md b/packages/services/service-cache/README.md index a92cedc4f95..616a36191ff 100644 --- a/packages/services/service-cache/README.md +++ b/packages/services/service-cache/README.md @@ -123,9 +123,16 @@ The slot is multi-provider. To back the cache with Redis, Memcached or anything register an object satisfying `ICacheService` under `'cache'` from your own plugin: ```typescript -import type { ICacheService } from '@objectstack/spec/contracts'; - -class MyCache implements ICacheService { /* the six members above */ } +import type { CacheStats, ICacheService } from '@objectstack/spec/contracts'; + +class MyCache implements ICacheService { + async get(key: string): Promise { /* … */ return undefined; } + async set(key: string, value: T, ttl?: number): Promise { /* … */ } + async delete(key: string): Promise { /* … */ return false; } + async has(key: string): Promise { /* … */ return false; } + async clear(): Promise { /* … */ } + async stats(): Promise { return { hits: 0, misses: 0, keyCount: 0 }; } +} // inside your plugin's init(ctx): ctx.registerService('cache', new MyCache()); diff --git a/packages/services/service-i18n/README.md b/packages/services/service-i18n/README.md index c6cd5d7ac59..2b338ba5a7b 100644 --- a/packages/services/service-i18n/README.md +++ b/packages/services/service-i18n/README.md @@ -150,9 +150,10 @@ carries the **active locale** so requests send a matching `Accept-Language`, and translations are fetched through `@objectstack/client`: ```tsx +import type { ObjectStackClient } from '@objectstack/client'; import { ObjectStackProvider, useObjectStackLocale } from '@objectstack/client-react'; -function App({ client, language }) { +function App({ client, language }: { client: ObjectStackClient; language: string }) { return ( diff --git a/packages/services/service-job/README.md b/packages/services/service-job/README.md index 7eb13c91b43..8da677b0d61 100644 --- a/packages/services/service-job/README.md +++ b/packages/services/service-job/README.md @@ -29,7 +29,7 @@ await jobs.schedule( 'daily_report', { type: 'cron', expression: '0 9 * * *', timezone: 'America/New_York' }, async ({ jobId }) => { await generateReport(jobId); }, - { retryPolicy: { maxRetries: 2, backoffMs: 1000 }, timeout: 60_000 }, + { retryPolicy: { maxRetries: 2, backoffMs: 1000 }, timeoutMs: 60_000 }, ); ``` diff --git a/packages/services/service-package/README.md b/packages/services/service-package/README.md index d019c753494..28ff8597aa5 100644 --- a/packages/services/service-package/README.md +++ b/packages/services/service-package/README.md @@ -30,7 +30,7 @@ import { PackageServicePlugin, type PackageService } from '@objectstack/service- const kernel = new ObjectKernel(); // Register after a driver/ObjectQL plugin so `ctx.getService('objectql')` resolves. -kernel.use(new PackageServicePlugin()); +await kernel.use(new PackageServicePlugin()); await kernel.bootstrap(); @@ -39,6 +39,7 @@ const packages = kernel.getService('package')!; await packages.publish({ manifest: { id: 'crm', + type: 'app', version: '1.2.0', name: 'CRM Package', /* …full ObjectStackManifest… */ diff --git a/packages/services/service-queue/README.md b/packages/services/service-queue/README.md index bb3244761db..2e13e19b176 100644 --- a/packages/services/service-queue/README.md +++ b/packages/services/service-queue/README.md @@ -32,14 +32,15 @@ pnpm add @objectstack/service-queue ```typescript import { ObjectKernel } from '@objectstack/core'; +import type { IQueueService } from '@objectstack/spec/contracts'; import { QueueServicePlugin } from '@objectstack/service-queue'; const kernel = new ObjectKernel(); // 'auto' (default): durable DbQueueAdapter when ObjectQL is available, else memory -kernel.use(new QueueServicePlugin({ adapter: 'auto' })); +await kernel.use(new QueueServicePlugin({ adapter: 'auto' })); await kernel.bootstrap(); -const queue = kernel.getService('queue'); // IQueueService +const queue = kernel.getService('queue'); // Publish a message await queue.subscribe('email', async (msg) => { @@ -47,8 +48,8 @@ await queue.subscribe('email', async (msg) => { }); await queue.publish('email', { to: 'user@example.com', template: 'welcome' }, { - // delay / priority / retries (see QueuePublishOptions) - attempts: 3, + // delay / priority / backoff / idempotencyKey (see QueuePublishOptions) + maxAttempts: 3, }); ``` diff --git a/packages/services/service-realtime/README.md b/packages/services/service-realtime/README.md index c543791e818..9a40eb6ecef 100644 --- a/packages/services/service-realtime/README.md +++ b/packages/services/service-realtime/README.md @@ -32,13 +32,14 @@ An HA adapter (Redis-backed, over `service-cluster-redis`) is a post-GA fast-fol ```typescript import { ObjectKernel } from '@objectstack/core'; +import type { IRealtimeService } from '@objectstack/spec/contracts'; import { RealtimeServicePlugin } from '@objectstack/service-realtime'; const kernel = new ObjectKernel(); -kernel.use(new RealtimeServicePlugin()); +await kernel.use(new RealtimeServicePlugin()); await kernel.bootstrap(); -const realtime = kernel.getService('realtime'); +const realtime = kernel.getService('realtime'); const subId = await realtime.subscribe('records', (event) => { console.log(event.type, event.payload); @@ -92,13 +93,21 @@ is wired without upgrading that row with a real enforcement site. Implements `IRealtimeService` from `@objectstack/spec/contracts`: ```typescript +import type { + RealtimeEventHandler, + RealtimeEventPayload, + RealtimeSubscriptionFilter, + RealtimeSubscriptionOptions, +} from '@objectstack/spec/contracts'; + interface IRealtimeService { publish(event: RealtimeEventPayload): Promise; subscribe(channel: string, handler: RealtimeEventHandler, options?: RealtimeSubscriptionOptions): Promise; unsubscribe(subscriptionId: string): Promise; handleUpgrade?(request: Request): Promise; // deliberately unimplemented — see above - subscribeMetadata?(filter, handler): Promise; // optional convenience — not implemented here - subscribeData?(filter, handler): Promise; // optional convenience — not implemented here + // optional convenience methods — not implemented here + subscribeMetadata?(filter: RealtimeSubscriptionFilter, handler: RealtimeEventHandler): Promise; + subscribeData?(filter: RealtimeSubscriptionFilter, handler: RealtimeEventHandler): Promise; } ``` diff --git a/packages/services/service-storage/README.md b/packages/services/service-storage/README.md index 9a1aa73a879..63a9b5a05aa 100644 --- a/packages/services/service-storage/README.md +++ b/packages/services/service-storage/README.md @@ -26,17 +26,18 @@ pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner ```typescript import { ObjectKernel } from '@objectstack/core'; +import type { IStorageService } from '@objectstack/spec/contracts'; import { StorageServicePlugin } from '@objectstack/service-storage'; const kernel = new ObjectKernel(); -kernel.use(new StorageServicePlugin({ +await kernel.use(new StorageServicePlugin({ adapter: 'local', local: { rootDir: './uploads' }, })); await kernel.bootstrap(); // Programmatic access -const storage = kernel.getService('storage'); +const storage = kernel.getService('storage'); await storage.upload('files/hello.txt', Buffer.from('hello')); ``` diff --git a/packages/spec/README.md b/packages/spec/README.md index 0fe1f7d04a0..a687d41cbb8 100644 --- a/packages/spec/README.md +++ b/packages/spec/README.md @@ -98,7 +98,7 @@ export const Task = ObjectSchema.create({ import { ObjectSchema } from '@objectstack/spec/data'; // Validate a JSON object against the schema -const result = ObjectSchema.parse(myObjectDefinition); +const result = ObjectSchema.safeParse(myObjectDefinition); if (result.success) { console.log('Valid object:', result.data); } diff --git a/packages/types/README.md b/packages/types/README.md index ef25ca64d7d..c0bc9ac491c 100644 --- a/packages/types/README.md +++ b/packages/types/README.md @@ -255,8 +255,9 @@ This package contains **only types**. Never import implementation details: // ✅ Good - type-only import import type { RuntimePlugin } from '@objectstack/types'; -// ❌ Bad - trying to import implementation -import { RuntimePlugin } from '@objectstack/types'; // Won't work +// ❌ Bad - trying to import the implementation. There is none, so a value +// import does not work: +// import { RuntimePlugin } from '@objectstack/types'; ``` ### 3. Extend Interfaces When Needed