Skip to main content

TypeScript Customization

Keep generated domain code replaceable and keep Node infrastructure out of browser packages. Runtime policy, provider resources, identity, and sinks are assembled by the server; the browser can request intent but cannot approve it.

Node context factory​

type TrustedRequest = Readonly<{
tenantId: bigint;
actorId: string;
permissions: ReadonlySet<string>;
}>;

async function requestContext(
infrastructure: Infrastructure,
trusted: TrustedRequest,
): Promise<UserContext> {
const context = new UserContext();
// Register generated metadata, explicit Node provider, policy and audit sinks.
// Install only authenticated server values.
return context;
}

Use the released @teaql/teaql APIs and documented export paths. Do not expose provider construction to route bodies and do not pass a service as a second execution argument.

Here is the concrete composition shape from Runtime Customization Assist:

interface AppAuditEvent { readonly [key: string]: unknown }
type AppAuditSink = (event: AppAuditEvent) => void | Promise<void>;

interface RequestPolicy {
readonly authorize: (operation: string, entity: string) => boolean;
}

interface RuntimeDataService extends TeaQLDataService {
setAuditSink(sink: AppAuditSink): this;
}

type RuntimeComposition = Readonly<{
dataService: RuntimeDataService;
requestPolicy: RequestPolicy;
appAuditSink: AppAuditSink;
trustedTenant: string;
}>;

function requestContext(composition: RuntimeComposition): UserContext {
if (!composition.trustedTenant.trim()) throw new Error("trusted tenant is required");
composition.dataService.setAuditSink(composition.appAuditSink);
return new UserContext()
.insertResource("dataService", composition.dataService)
.insertResource("requestPolicy", composition.requestPolicy)
.insertResource("appAuditSink", composition.appAuditSink)
.insertResource("trustedTenant", composition.trustedTenant);
}

async function readiness(context: UserContext): Promise<void> {
context.requireResource<RequestPolicy>("requestPolicy");
context.requireResource<AppAuditSink>("appAuditSink");
if (!context.requireResource<string>("trustedTenant").trim()) {
throw new Error("trusted tenant is required");
}
await context.ensureSchema();
}

The current core prepareQuery() does not automatically consume the stored requestPolicy. The server adapter/custom data service must enforce authorize(...) and tenant scope before provider execution; retaining the resource alone is not authorization.

Dynamic input adapter​

Parse unknown input, validate it, then map an allowlisted discriminated union to generated methods:

function applyFilter(req: CustomerOrderRequest, filter: Filter): CustomerOrderRequest {
switch (`${filter.field}:${filter.operator}`) {
case "order_number:contains":
return req.withOrderNumberContaining(requireString(filter.value));
default:
throw new InvalidQuery("Unsupported field or operator");
}
}

Reject unknown/deep paths, excessive IN/page size, reversed or wrongly typed ranges, negative offset and forbidden sort. Derive rows, count, totals, and facets from one normalized filter.

Policy, behavior, and audit​

Entity-local invariants belong in generated behavior/checker contracts. Tenant scope, permissions, approved purpose and platform limits belong in context request policy. Every mutation uses auditAs; the immutable runtime mutation event and separately registered masked application event are distinct evidence paths.

Provider exports​

Import SQL providers only through documented Node subpaths. Keep the root/browser export free of native modules and Node built-ins. Qualify each provider with real persistence, reconnect, optimistic locking, aggregates, pagination, and relation Top-N.

Federal customization​

Customize endpoint discovery, authentication transport, retry and tracing around the generated federal protocol client without replacing the protocol with arbitrary HTTP. The backend rebuilds trusted UserContext, validates the query allowlist, and decides whether requested purpose is approved.

Packaging​

Pin @teaql/teaql and preserve the lock file. Test Node ESM/CJS behavior and a clean browser bundle against the documented exports. SQL providers stay behind explicit subpaths; do not replace the published dependency with a local cache or preview tarball in production documentation.

If a change requires editing generated Q, Request, Entity, or expression source, fix KSML, generator, or runtime and regenerate.

Runtime infrastructure​

TypeScript exports LocalCache and the process singleton localCache with optional TTL. Local Lock is intentionally not applicable to the single-threaded local runtime, and TeaQL currently claims no TypeScript Remote Cache, Remote Lock, Nacos, or Consul adapter. Node applications may integrate external SDKs, but that is application infrastructure rather than portable TeaQL runtime support. See Cache, Lock, and Cloud Runtime Infrastructure.

Worked Example: Local Cache​

import { localCache } from "@teaql/teaql";

async function currencyView(
context: UserContext,
trustedTenant: string,
currencyCode: string,
): Promise<Readonly<CurrencyView>> {
const key = `${trustedTenant}:currency:${currencyCode}`;
const cached = localCache.get<Readonly<CurrencyView>>(key);
if (cached !== undefined) return cached;

const loaded = Object.freeze(
await loadAuthorizedCurrencyView(context, currencyCode),
);
localCache.put(key, loaded, 300);
return loaded;
}

function invalidateCurrency(trustedTenant: string, currencyCode: string): void {
localCache.remove(`${trustedTenant}:currency:${currencyCode}`);
}

Invalidate after the audited save succeeds. Node replicas do not share this cache. For distributed idempotency or coordination, use an application-owned database/Redis service with an owner-safe atomic contract; TeaQL TypeScript does not currently supply a Remote Lock abstraction.