🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@parcae/model

Package Overview
Dependencies
Maintainers
1
Versions
37
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@parcae/model - npm Package Compare versions

Comparing version
0.7.0
to
0.8.0
+141
dist/chunk-7RSAGEBY.js
import { CHAINABLE_METHODS } from './chunk-F5LRP7SS.js';
import { proxy } from 'valtio';
function stripVersion(path) {
return path.replace(/^\/v\d+/, "");
}
var FrontendAdapter = class {
transport;
constructor(transport) {
this.transport = transport;
}
p(modelPath) {
return stripVersion(modelPath);
}
// ── createStore ──────────────────────────────────────────────────────
createStore(data) {
return proxy(data);
}
// ── save ─────────────────────────────────────────────────────────────
async save(model, changes) {
const ModelClass = model.constructor;
const path = this.resolvePath(ModelClass);
if (changes.creating) {
const result = await this.transport.post(path, model.__data);
if (result?.id) model.id = result.id;
} else if (changes.ops.length > 0) {
await this.transport.patch(`${path}/${model.id}`, { ops: changes.ops });
} else if (changes.updates.length > 0) {
const data = {};
for (const key of changes.updates) {
data[key] = model.__data[key];
}
await this.transport.put(`${path}/${model.id}`, data);
}
}
// ── remove ───────────────────────────────────────────────────────────
async remove(model) {
const ModelClass = model.constructor;
const path = this.resolvePath(ModelClass);
await this.transport.delete(`${path}/${model.id}`);
}
// ── findById ─────────────────────────────────────────────────────────
async findById(modelClass, id) {
const path = this.resolvePath(modelClass);
try {
const data = await this.transport.get(`${path}/${id}`);
if (!data) return null;
return new modelClass(this, data);
} catch {
return null;
}
}
// ── query ────────────────────────────────────────────────────────────
query(modelClass) {
return this._buildQuery(modelClass, []);
}
_buildQuery(modelClass, steps) {
const adapter = this;
const chain = {};
for (const method of CHAINABLE_METHODS) {
chain[method] = (...args) => {
return adapter._buildQuery(modelClass, [
...steps,
{ method, args: adapter._serializeArgs(args) }
]);
};
}
chain.find = async () => {
const path = adapter.resolvePath(modelClass);
const result = await adapter.transport.get(path, { __query: steps });
const items = result?.[modelClass.type + "s"] ?? result?.items ?? [];
const models = items.map((row) => new modelClass(adapter, row));
if (result?.__queryHash) {
Object.defineProperty(models, "__queryHash", {
value: result.__queryHash,
enumerable: false
});
}
return models;
};
chain.first = async () => {
const all = await chain.find();
return all[0] ?? null;
};
chain.count = async () => {
const path = adapter.resolvePath(modelClass);
const result = await adapter.transport.get(path, {
__query: steps,
__count: true
});
return result?.total ?? 0;
};
chain.__steps = steps;
chain.__modelType = modelClass.type;
chain.__modelClass = modelClass;
chain.__adapter = adapter;
return chain;
}
_serializeArgs(args) {
return args.map((arg) => {
if (typeof arg === "function") {
const nestedSteps = [];
const recorder = new Proxy(
{},
{
get: (_target, method) => {
return (...innerArgs) => {
nestedSteps.push({
method,
args: this._serializeArgs(innerArgs)
});
return recorder;
};
}
}
);
arg(recorder);
return { __nested: nestedSteps };
}
return arg;
});
}
// ── patch ────────────────────────────────────────────────────────────
async patch(model, ops) {
const ModelClass = model.constructor;
const path = this.resolvePath(ModelClass);
await this.transport.patch(`${path}/${model.id}`, { ops });
}
// ── helpers ──────────────────────────────────────────────────────────
resolvePath(modelClass) {
if (!modelClass.path && !modelClass.type) {
throw new Error(`Model has no path or type`);
}
const fullPath = modelClass.path ?? `/v1/${modelClass.type}s`;
return this.p(fullPath);
}
};
export { FrontendAdapter };
//# sourceMappingURL=chunk-7RSAGEBY.js.map
//# sourceMappingURL=chunk-7RSAGEBY.js.map
{"version":3,"sources":["../src/adapters/client.ts"],"names":[],"mappings":"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","file":"chunk-7RSAGEBY.js","sourcesContent":["/**\n * FrontendAdapter — Valtio + Transport for Parcae Model.\n *\n * Transport-agnostic. Works with any implementation of the Transport interface:\n * - SocketTransport (Socket.IO — bidirectional, realtime)\n * - SSETransport (Server-Sent Events — read-heavy, simpler)\n * - GRPCTransport (gRPC — service-to-service)\n * - Or any custom transport\n */\n\nimport { proxy } from \"valtio\";\nimport {\n CHAINABLE_METHODS,\n type ModelAdapter,\n type ModelConstructor,\n type ChangeSet,\n type QueryChain,\n type QueryStep,\n type PatchOp,\n} from \"./types\";\n\n// ─── Transport Interface ─────────────────────────────────────────────────────\n\n/** Per-request options that callers can pass to any RPC method. */\nexport interface RequestOptions {\n /** Override the default RPC timeout (in milliseconds). */\n timeout?: number;\n}\n\n/**\n * Abstract transport layer. Decouples the Model system from the wire protocol.\n *\n * Implementations:\n * - SocketTransport: Socket.IO (bidirectional, realtime subscriptions)\n * - SSETransport: HTTP + Server-Sent Events (read-heavy, simpler infra)\n * - GRPCTransport: gRPC (service-to-service, protobuf)\n */\nexport interface Transport {\n // ── Request/Response (RPC-style) ────────────────────────────────────\n get(path: string, data?: any, options?: RequestOptions): Promise<any>;\n post(path: string, data?: any, options?: RequestOptions): Promise<any>;\n put(path: string, data?: any, options?: RequestOptions): Promise<any>;\n patch(path: string, data?: any, options?: RequestOptions): Promise<any>;\n delete(path: string, data?: any, options?: RequestOptions): Promise<any>;\n\n // ── Subscriptions (realtime) ────────────────────────────────────────\n /**\n * Subscribe to a named event stream.\n * Returns a dispose function to unsubscribe.\n *\n * Socket.IO: socket.on(event, handler)\n * SSE: new EventSource(url) per channel\n * gRPC: server streaming RPC\n */\n subscribe?(event: string, handler: (...args: any[]) => void): () => void;\n\n /**\n * Unsubscribe from a named event stream.\n * Alternative to calling the dispose function returned by subscribe().\n */\n unsubscribe?(event: string, handler?: (...args: any[]) => void): void;\n\n // ── Control messages ────────────────────────────────────────────────\n /**\n * Send a control message to the server (fire-and-forget).\n *\n * Socket.IO: socket.emit(event, ...args)\n * SSE: POST /v1/__control { event, args }\n * gRPC: unary RPC\n */\n send?(event: string, ...args: any[]): void;\n\n // ── Connection state ────────────────────────────────────────────────\n /** Whether the transport is currently connected. */\n readonly isConnected?: boolean;\n /** Whether the transport is currently loading/connecting. */\n readonly isLoading?: boolean;\n\n /**\n * Listen for connection state changes.\n * Events: \"connected\", \"disconnected\", \"reconnecting\", \"error\"\n */\n on?(event: string, handler: (...args: any[]) => void): void;\n off?(event: string, handler?: (...args: any[]) => void): void;\n\n /** Disconnect the transport. */\n disconnect?(): void;\n /** Reconnect the transport. */\n reconnect?(): Promise<void>;\n}\n\n/**\n * Strip version prefix from a path if it starts with /v{number}.\n * The transport prepends the version automatically.\n */\nfunction stripVersion(path: string): string {\n return path.replace(/^\\/v\\d+/, \"\");\n}\n\n// ─── FrontendAdapter ─────────────────────────────────────────────────────────\n\nexport class FrontendAdapter implements ModelAdapter {\n public transport: Transport;\n\n constructor(transport: Transport) {\n this.transport = transport;\n }\n\n private p(modelPath: string): string {\n return stripVersion(modelPath);\n }\n\n // ── createStore ──────────────────────────────────────────────────────\n\n createStore(data: Record<string, any>): Record<string, any> {\n return proxy(data);\n }\n\n // ── save ─────────────────────────────────────────────────────────────\n\n async save(model: any, changes: ChangeSet): Promise<void> {\n const ModelClass = model.constructor as ModelConstructor;\n const path = this.resolvePath(ModelClass);\n\n if (changes.creating) {\n const result = await this.transport.post(path, model.__data);\n if (result?.id) (model as any).id = result.id;\n } else if (changes.ops.length > 0) {\n await this.transport.patch(`${path}/${model.id}`, { ops: changes.ops });\n } else if (changes.updates.length > 0) {\n const data: Record<string, any> = {};\n for (const key of changes.updates) {\n data[key] = model.__data[key];\n }\n await this.transport.put(`${path}/${model.id}`, data);\n }\n }\n\n // ── remove ───────────────────────────────────────────────────────────\n\n async remove(model: any): Promise<void> {\n const ModelClass = model.constructor as ModelConstructor;\n const path = this.resolvePath(ModelClass);\n await this.transport.delete(`${path}/${model.id}`);\n }\n\n // ── findById ─────────────────────────────────────────────────────────\n\n async findById<T>(\n modelClass: ModelConstructor<T>,\n id: string,\n ): Promise<T | null> {\n const path = this.resolvePath(modelClass);\n try {\n const data = await this.transport.get(`${path}/${id}`);\n if (!data) return null;\n return new modelClass(this, data) as T;\n } catch {\n return null;\n }\n }\n\n // ── query ────────────────────────────────────────────────────────────\n\n query<T>(modelClass: ModelConstructor<T>): QueryChain<T> {\n return this._buildQuery(modelClass, []);\n }\n\n private _buildQuery<T>(\n modelClass: ModelConstructor<T>,\n steps: QueryStep[],\n ): QueryChain<T> {\n const adapter = this;\n\n const chain: any = {};\n\n for (const method of CHAINABLE_METHODS) {\n chain[method] = (...args: any[]) => {\n return adapter._buildQuery(modelClass, [\n ...steps,\n { method, args: adapter._serializeArgs(args) },\n ]);\n };\n }\n\n chain.find = async (): Promise<T[]> => {\n const path = adapter.resolvePath(modelClass);\n const result = await adapter.transport.get(path, { __query: steps });\n const items = result?.[modelClass.type + \"s\"] ?? result?.items ?? [];\n const models = items.map((row: any) => new modelClass(adapter, row));\n // Attach query subscription hash if backend provided one\n if (result?.__queryHash) {\n Object.defineProperty(models, \"__queryHash\", {\n value: result.__queryHash,\n enumerable: false,\n });\n }\n return models;\n };\n\n chain.first = async (): Promise<T | null> => {\n const all = await chain.find();\n return all[0] ?? null;\n };\n\n chain.count = async (): Promise<number> => {\n const path = adapter.resolvePath(modelClass);\n const result = await adapter.transport.get(path, {\n __query: steps,\n __count: true,\n });\n return result?.total ?? 0;\n };\n\n chain.__steps = steps;\n chain.__modelType = modelClass.type;\n chain.__modelClass = modelClass;\n chain.__adapter = adapter;\n\n return chain as QueryChain<T>;\n }\n\n private _serializeArgs(args: any[]): any[] {\n return args.map((arg) => {\n if (typeof arg === \"function\") {\n // Capture nested builder calls: .where((b) => b.where(...).orWhere(...))\n const nestedSteps: QueryStep[] = [];\n const recorder: any = new Proxy(\n {},\n {\n get: (_target, method: string) => {\n return (...innerArgs: any[]) => {\n nestedSteps.push({\n method,\n args: this._serializeArgs(innerArgs),\n });\n return recorder;\n };\n },\n },\n );\n arg(recorder);\n return { __nested: nestedSteps };\n }\n return arg;\n });\n }\n\n // ── patch ────────────────────────────────────────────────────────────\n\n async patch(model: any, ops: PatchOp[]): Promise<void> {\n const ModelClass = model.constructor as ModelConstructor;\n const path = this.resolvePath(ModelClass);\n await this.transport.patch(`${path}/${model.id}`, { ops });\n }\n\n // ── helpers ──────────────────────────────────────────────────────────\n\n private resolvePath(modelClass: ModelConstructor): string {\n if (!modelClass.path && !modelClass.type) {\n throw new Error(`Model has no path or type`);\n }\n const fullPath = modelClass.path ?? `/v1/${modelClass.type}s`;\n return this.p(fullPath);\n }\n}\n"]}
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/adapters/types.ts
var CHAINABLE_METHODS = [
"select",
"where",
"andWhere",
"orWhere",
"whereIn",
"whereNot",
"whereNotIn",
"whereNull",
"whereNotNull",
"whereBetween",
"whereRaw",
"orWhereRaw",
"orWhereIn",
"orWhereNull",
"whereExists",
"search",
"orderBy",
"orderByRaw",
"groupBy",
"groupByRaw",
"having",
"havingRaw",
"limit",
"offset",
"distinct",
"distinctOn",
"join",
"innerJoin",
"leftJoin",
"rightJoin",
"clearOrder",
"clearSelect",
"from",
"sum",
"avg",
"min",
"max",
"increment",
"decrement"
];
export { CHAINABLE_METHODS, __export };
//# sourceMappingURL=chunk-F5LRP7SS.js.map
//# sourceMappingURL=chunk-F5LRP7SS.js.map
{"version":3,"sources":["../src/adapters/types.ts"],"names":[],"mappings":";;;;;;;AAoRO,IAAM,iBAAA,GAAoB;AAAA,EAC/B,QAAA;AAAA,EACA,OAAA;AAAA,EACA,UAAA;AAAA,EACA,SAAA;AAAA,EACA,SAAA;AAAA,EACA,UAAA;AAAA,EACA,YAAA;AAAA,EACA,WAAA;AAAA,EACA,cAAA;AAAA,EACA,cAAA;AAAA,EACA,UAAA;AAAA,EACA,YAAA;AAAA,EACA,WAAA;AAAA,EACA,aAAA;AAAA,EACA,aAAA;AAAA,EACA,QAAA;AAAA,EACA,SAAA;AAAA,EACA,YAAA;AAAA,EACA,SAAA;AAAA,EACA,YAAA;AAAA,EACA,QAAA;AAAA,EACA,WAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,UAAA;AAAA,EACA,YAAA;AAAA,EACA,MAAA;AAAA,EACA,WAAA;AAAA,EACA,UAAA;AAAA,EACA,WAAA;AAAA,EACA,YAAA;AAAA,EACA,aAAA;AAAA,EACA,MAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,WAAA;AAAA,EACA;AACF","file":"chunk-F5LRP7SS.js","sourcesContent":["/**\n * @parcae/model — Adapter Types\n *\n * Defines the adapter interface that decouples Model from persistence.\n * Frontend: Valtio + Socket.IO transport\n * Backend: Knex + PostgreSQL\n */\n\nimport type { Operation as PatchOp } from \"fast-json-patch\";\n\n// ─── Column Types ────────────────────────────────────────────────────────────\n\n/**\n * Primitive column types that map to Postgres column types.\n * These are resolved from TypeScript types via RTTIST at startup.\n */\nexport type PrimitiveColumnType =\n | \"string\" // VARCHAR(2048)\n | \"text\" // TEXT\n | \"integer\" // INTEGER\n | \"number\" // DOUBLE PRECISION\n | \"boolean\" // BOOLEAN\n | \"datetime\" // TIMESTAMP\n | \"json\"; // JSONB\n\n/**\n * A column can be a primitive type or a reference to another Model class.\n */\nexport type ColumnType =\n | PrimitiveColumnType\n | { kind: \"ref\"; target: ModelConstructor };\n\n/**\n * Schema definition for a model, mapping property names to column types.\n * Generated by the RTTIST schema resolver at startup.\n */\nexport type SchemaDefinition = Record<string, ColumnType>;\n\n/**\n * Index definition — a single column name or a composite index.\n */\nexport type IndexDefinition = string | string[];\n\n// ─── Model Constructor ───────────────────────────────────────────────────────\n\n/**\n * The static side of a Model subclass.\n */\nexport interface ModelConstructor<T = any> {\n type: string;\n path?: string;\n scope?: ModelScope;\n indexes?: IndexDefinition[];\n managed?: boolean;\n /**\n * Fields to index for full-text and fuzzy search.\n * When present, `ensureTable()` creates:\n * - A generated `_search` tsvector column + GIN index\n * - Per-field trigram GIN indexes (pg_trgm)\n * - On AlloyDB: `_embedding` vector(768) column + ScaNN index\n *\n * Fields are weighted by order: first = 'A' (highest), second = 'B', etc.\n *\n * @example\n * ```typescript\n * class Project extends Model {\n * static searchFields = [\"title\", \"description\"];\n * }\n * // Then query with: Project.search(\"ghost town\").find()\n * ```\n */\n searchFields?: string[];\n __schema?: SchemaDefinition;\n new (adapter: ModelAdapter, data?: Record<string, any>): T;\n}\n\n// ─── Scope System ────────────────────────────────────────────────────────────\n\n/**\n * Context passed to scope functions. Contains the authenticated user\n * and request parameters.\n */\nexport interface ScopeContext {\n user?: { id: string; [key: string]: any } | null;\n params?: Record<string, string>;\n data?: Record<string, any>;\n}\n\n/**\n * The result of a scope function. Either:\n * - A query builder modifier function (for read/update/delete/patch)\n * - A data object to merge (for create — e.g. { user: ctx.user.id })\n * - null to deny access\n */\nexport type ScopeResult = ((qb: any) => any) | Record<string, any> | null;\n\n/**\n * A scope function receives the request context and returns a ScopeResult.\n */\nexport type ScopeFunction = (ctx: ScopeContext) => ScopeResult;\n\n/**\n * Per-operation scope definitions.\n */\nexport interface ModelScope {\n read?: ScopeFunction;\n create?: ScopeFunction;\n update?: ScopeFunction;\n delete?: ScopeFunction;\n patch?: ScopeFunction;\n}\n\n// ─── Change Tracking ─────────────────────────────────────────────────────────\n\n/**\n * A set of changes to flush to the adapter.\n */\nexport interface ChangeSet {\n /** Property names that were set via direct assignment */\n updates: string[];\n /** RFC 6902 JSON Patch operations */\n ops: PatchOp[];\n /** Whether this is a new model being created */\n creating: boolean;\n}\n\n// ─── Query Chain ─────────────────────────────────────────────────────────────\n\n/**\n * A serializable query step. On the frontend, query chains record steps\n * as these objects and send them to the backend for execution.\n */\nexport interface QueryStep {\n method: string;\n args: any[];\n}\n\n/**\n * Chainable query builder. The generic T flows through to terminal methods.\n *\n * On the backend, each method directly mutates a Knex query.\n * On the frontend, each method records a QueryStep for serialization.\n */\nexport interface QueryChain<T> {\n // Chainable — filtering\n where(...args: any[]): QueryChain<T>;\n andWhere(...args: any[]): QueryChain<T>;\n orWhere(...args: any[]): QueryChain<T>;\n whereIn(column: string, values: any[]): QueryChain<T>;\n whereNot(...args: any[]): QueryChain<T>;\n whereNotIn(column: string, values: any[]): QueryChain<T>;\n whereNull(column: string): QueryChain<T>;\n whereNotNull(column: string): QueryChain<T>;\n whereBetween(column: string, range: [any, any]): QueryChain<T>;\n whereRaw(query: string, ...bindings: any[]): QueryChain<T>;\n orWhereRaw(query: string, ...bindings: any[]): QueryChain<T>;\n orWhereIn(column: string, values: any[]): QueryChain<T>;\n orWhereNull(column: string): QueryChain<T>;\n whereExists(callback: any): QueryChain<T>;\n\n // Chainable — search\n /**\n * Full-text + fuzzy search across fields declared in `static search`.\n * On the backend, generates hybrid SQL (tsvector + trigram + optional vector).\n * On the frontend, records a QueryStep for serialization.\n */\n search(term: string): QueryChain<T>;\n\n // Chainable — ordering & pagination\n orderBy(column: string, direction?: \"asc\" | \"desc\"): QueryChain<T>;\n orderByRaw(query: string, ...bindings: any[]): QueryChain<T>;\n limit(count: number): QueryChain<T>;\n offset(count: number): QueryChain<T>;\n\n // Chainable — selection & grouping\n select(...columns: string[]): QueryChain<T>;\n distinct(...columns: string[]): QueryChain<T>;\n distinctOn(...columns: string[]): QueryChain<T>;\n groupBy(...columns: string[]): QueryChain<T>;\n groupByRaw(query: string, ...bindings: any[]): QueryChain<T>;\n having(...args: any[]): QueryChain<T>;\n havingRaw(query: string, ...bindings: any[]): QueryChain<T>;\n\n // Chainable — joins\n join(...args: any[]): QueryChain<T>;\n innerJoin(...args: any[]): QueryChain<T>;\n leftJoin(...args: any[]): QueryChain<T>;\n rightJoin(...args: any[]): QueryChain<T>;\n\n // Chainable — modifiers\n clearOrder(): QueryChain<T>;\n clearSelect(): QueryChain<T>;\n from(table: string): QueryChain<T>;\n\n // Chainable — aggregates (chainable versions)\n sum(column: string): QueryChain<T>;\n avg(column: string): QueryChain<T>;\n min(column: string): QueryChain<T>;\n max(column: string): QueryChain<T>;\n increment(column: string, amount?: number): QueryChain<T>;\n decrement(column: string, amount?: number): QueryChain<T>;\n\n // Convenience\n basic(\n limit?: number,\n sort?: string,\n direction?: \"asc\" | \"desc\",\n page?: number,\n ): QueryChain<T>;\n\n // Terminal — execute the query\n find(): Promise<T[]>;\n first(): Promise<T | null>;\n count(): Promise<number>;\n\n // Introspection — access underlying query builder\n /** @internal Return the underlying query builder (e.g. Knex QueryBuilder). */\n exec(): any;\n /** @internal Create an independent copy of this chain. */\n clone(): QueryChain<T>;\n\n // Internal — used by hooks and subscription manager\n /** @internal */ __steps: QueryStep[];\n /** @internal */ __modelType: string;\n /** @internal */ __modelClass: ModelConstructor<T>;\n /** @internal */ __adapter: ModelAdapter;\n /** @internal */ __debounceMs?: number;\n}\n\n// ─── Model Adapter ───────────────────────────────────────────────────────────\n\n/**\n * The adapter interface. Decouples Model from persistence.\n *\n * - FrontendAdapter: Valtio proxy + Socket.IO transport\n * - BackendAdapter: Knex + PostgreSQL + hooks + pub/sub\n */\nexport interface ModelAdapter {\n /** Create the reactive data store. Frontend: Valtio proxy. Backend: plain object. */\n createStore(data: Record<string, any>): Record<string, any>;\n\n /** Persist changes (create or update). */\n save(model: any, changes: ChangeSet): Promise<void>;\n\n /** Delete a model. */\n remove(model: any): Promise<void>;\n\n /** Fetch a single model by ID. */\n findById<T>(modelClass: ModelConstructor<T>, id: string): Promise<T | null>;\n\n /** Start a query chain. */\n query<T>(modelClass: ModelConstructor<T>): QueryChain<T>;\n\n /**\n * Build a scoped query from client-sent QueryStep[] (backend only).\n * Safe replay of __query steps with scope applied first.\n */\n queryFromClient?<T>(\n modelClass: ModelConstructor<T>,\n scope: Record<string, any>,\n rawSteps: QueryStep[] | string | undefined,\n ): QueryChain<T>;\n\n /** Apply atomic RFC 6902 JSON Patch operations. */\n patch(model: any, ops: PatchOp[]): Promise<void>;\n}\n\nexport type { PatchOp };\n\n// ─── Chainable Methods ───────────────────────────────────────────────────────\n\n/**\n * The set of query chain methods that are chainable (return a new QueryChain).\n * Shared across lazyQuery, FrontendAdapter, and BackendAdapter to avoid\n * the same 37-element array being duplicated three times.\n */\nexport const CHAINABLE_METHODS = [\n \"select\",\n \"where\",\n \"andWhere\",\n \"orWhere\",\n \"whereIn\",\n \"whereNot\",\n \"whereNotIn\",\n \"whereNull\",\n \"whereNotNull\",\n \"whereBetween\",\n \"whereRaw\",\n \"orWhereRaw\",\n \"orWhereIn\",\n \"orWhereNull\",\n \"whereExists\",\n \"search\",\n \"orderBy\",\n \"orderByRaw\",\n \"groupBy\",\n \"groupByRaw\",\n \"having\",\n \"havingRaw\",\n \"limit\",\n \"offset\",\n \"distinct\",\n \"distinctOn\",\n \"join\",\n \"innerJoin\",\n \"leftJoin\",\n \"rightJoin\",\n \"clearOrder\",\n \"clearSelect\",\n \"from\",\n \"sum\",\n \"avg\",\n \"min\",\n \"max\",\n \"increment\",\n \"decrement\",\n] as const;\n"]}
+11
-6

@@ -14,2 +14,7 @@ import { ModelAdapter, ChangeSet, ModelConstructor, QueryChain } from './types.js';

/** Per-request options that callers can pass to any RPC method. */
interface RequestOptions {
/** Override the default RPC timeout (in milliseconds). */
timeout?: number;
}
/**

@@ -24,7 +29,7 @@ * Abstract transport layer. Decouples the Model system from the wire protocol.

interface Transport {
get(path: string, data?: any): Promise<any>;
post(path: string, data?: any): Promise<any>;
put(path: string, data?: any): Promise<any>;
patch(path: string, data?: any): Promise<any>;
delete(path: string, data?: any): Promise<any>;
get(path: string, data?: any, options?: RequestOptions): Promise<any>;
post(path: string, data?: any, options?: RequestOptions): Promise<any>;
put(path: string, data?: any, options?: RequestOptions): Promise<any>;
patch(path: string, data?: any, options?: RequestOptions): Promise<any>;
delete(path: string, data?: any, options?: RequestOptions): Promise<any>;
/**

@@ -82,2 +87,2 @@ * Subscribe to a named event stream.

export { FrontendAdapter, type Transport, FrontendAdapter as default };
export { FrontendAdapter, type RequestOptions, type Transport };

@@ -1,3 +0,4 @@

export { FrontendAdapter, client_default as default } from '../chunk-UE7DW2FG.js';
export { FrontendAdapter } from '../chunk-7RSAGEBY.js';
import '../chunk-F5LRP7SS.js';
//# sourceMappingURL=client.js.map
//# sourceMappingURL=client.js.map

@@ -42,2 +42,20 @@ import { Operation } from 'fast-json-patch';

managed?: boolean;
/**
* Fields to index for full-text and fuzzy search.
* When present, `ensureTable()` creates:
* - A generated `_search` tsvector column + GIN index
* - Per-field trigram GIN indexes (pg_trgm)
* - On AlloyDB: `_embedding` vector(768) column + ScaNN index
*
* Fields are weighted by order: first = 'A' (highest), second = 'B', etc.
*
* @example
* ```typescript
* class Project extends Model {
* static searchFields = ["title", "description"];
* }
* // Then query with: Project.search("ghost town").find()
* ```
*/
searchFields?: string[];
__schema?: SchemaDefinition;

@@ -119,2 +137,8 @@ new (adapter: ModelAdapter, data?: Record<string, any>): T;

whereExists(callback: any): QueryChain<T>;
/**
* Full-text + fuzzy search across fields declared in `static search`.
* On the backend, generates hybrid SQL (tsvector + trigram + optional vector).
* On the frontend, records a QueryStep for serialization.
*/
search(term: string): QueryChain<T>;
orderBy(column: string, direction?: "asc" | "desc"): QueryChain<T>;

@@ -146,9 +170,8 @@ orderByRaw(query: string, ...bindings: any[]): QueryChain<T>;

find(): Promise<T[]>;
/** Like find(), but also subscribes to realtime updates via the QSM. */
findAndSubscribe(): Promise<{
items: T[];
hash: string | null;
}>;
first(): Promise<T | null>;
count(): Promise<number>;
/** @internal Return the underlying query builder (e.g. Knex QueryBuilder). */
exec(): any;
/** @internal Create an independent copy of this chain. */
clone(): QueryChain<T>;
/** @internal */ __steps: QueryStep[];

@@ -177,2 +200,7 @@ /** @internal */ __modelType: string;

query<T>(modelClass: ModelConstructor<T>): QueryChain<T>;
/**
* Build a scoped query from client-sent QueryStep[] (backend only).
* Safe replay of __query steps with scope applied first.
*/
queryFromClient?<T>(modelClass: ModelConstructor<T>, scope: Record<string, any>, rawSteps: QueryStep[] | string | undefined): QueryChain<T>;
/** Apply atomic RFC 6902 JSON Patch operations. */

@@ -182,2 +210,9 @@ patch(model: any, ops: Operation[]): Promise<void>;

export type { ChangeSet, ColumnType, IndexDefinition, ModelAdapter, ModelConstructor, ModelScope, PrimitiveColumnType, QueryChain, QueryStep, SchemaDefinition, ScopeContext, ScopeFunction, ScopeResult };
/**
* The set of query chain methods that are chainable (return a new QueryChain).
* Shared across lazyQuery, FrontendAdapter, and BackendAdapter to avoid
* the same 37-element array being duplicated three times.
*/
declare const CHAINABLE_METHODS: readonly ["select", "where", "andWhere", "orWhere", "whereIn", "whereNot", "whereNotIn", "whereNull", "whereNotNull", "whereBetween", "whereRaw", "orWhereRaw", "orWhereIn", "orWhereNull", "whereExists", "search", "orderBy", "orderByRaw", "groupBy", "groupByRaw", "having", "havingRaw", "limit", "offset", "distinct", "distinctOn", "join", "innerJoin", "leftJoin", "rightJoin", "clearOrder", "clearSelect", "from", "sum", "avg", "min", "max", "increment", "decrement"];
export { CHAINABLE_METHODS, type ChangeSet, type ColumnType, type IndexDefinition, type ModelAdapter, type ModelConstructor, type ModelScope, type PrimitiveColumnType, type QueryChain, type QueryStep, type SchemaDefinition, type ScopeContext, type ScopeFunction, type ScopeResult };

@@ -1,3 +0,3 @@

export { CHAINABLE_METHODS } from '../chunk-F5LRP7SS.js';
//# sourceMappingURL=types.js.map
//# sourceMappingURL=types.js.map
import { EventEmitter } from 'eventemitter3';
import { ModelAdapter, SchemaDefinition, ModelConstructor, QueryChain } from './adapters/types.js';
export { ChangeSet, ColumnType, IndexDefinition, ModelScope, PrimitiveColumnType, QueryStep, ScopeContext, ScopeFunction, ScopeResult } from './adapters/types.js';
export { CHAINABLE_METHODS, ChangeSet, ColumnType, QueryStep, ScopeContext } from './adapters/types.js';
import { Operation } from 'fast-json-patch';
export { Operation as PatchOp } from 'fast-json-patch';
export { FrontendAdapter, Transport } from './adapters/client.js';
export { FrontendAdapter, RequestOptions, Transport } from './adapters/client.js';

@@ -28,2 +28,12 @@ /**

declare const SYM_INIT_DATA: unique symbol;
/**
* Symbol used to atomically merge server-authoritative data onto a Model
* instance, bypassing the Proxy set trap so that SYM_UPDATES (local
* dirty-tracking) is not polluted. Skips any keys that have pending
* local changes (SYM_UPDATES or SYM_PATCHING).
*
* Accessed via the Proxy get trap, which returns a closure over the raw
* target so writes bypass the Proxy entirely.
*/
declare const SYM_SERVER_MERGE: unique symbol;
declare class Model extends EventEmitter {

@@ -40,2 +50,3 @@ static type: string;

static indexes?: (string | string[])[];
static searchFields?: string[];
static managed: boolean;

@@ -65,3 +76,3 @@ /** @internal */

static count<T extends Model>(this: ModelConstructor<T>): Promise<number>;
static basic<T extends Model>(this: ModelConstructor<T>, limit?: number, sort?: string, direction?: "asc" | "desc", page?: number): QueryChain<T>;
static search<T extends Model>(this: ModelConstructor<T>, term: string): QueryChain<T>;
constructor(adapter: ModelAdapter, data?: Record<string, any>);

@@ -95,3 +106,2 @@ /**

refresh(): Promise<void>;
load(): Promise<void>;
sanitize(_user?: {

@@ -102,2 +112,3 @@ id: string;

private static __refCache;
private static REF_CACHE_TTL;
private _createRefProxy;

@@ -118,2 +129,18 @@ }

export { Model, ModelAdapter, ModelConstructor, QueryChain, SchemaDefinition, generateId };
/**
* @parcae/model
*
* The Parcae Model system — typed ORM base class with adapter pattern.
* Class properties ARE the schema. Direct property access. Full type safety.
*/
/**
* Branded type for unlimited TEXT columns (vs string which is VARCHAR 2048).
* Use `content: Text = ""` on a Model property to get a TEXT column in Postgres.
* At runtime it's just a string — the brand is erased.
*/
type Text = string & {
readonly __brand: "Text";
};
export { Model, ModelAdapter, ModelConstructor, QueryChain, SYM_SERVER_MERGE, SchemaDefinition, type Text, generateId };

@@ -1,6 +0,644 @@

export { FrontendAdapter } from './chunk-UE7DW2FG.js';
export { FrontendAdapter } from './chunk-7RSAGEBY.js';
import { __export, CHAINABLE_METHODS } from './chunk-F5LRP7SS.js';
export { CHAINABLE_METHODS } from './chunk-F5LRP7SS.js';
import { EventEmitter } from 'eventemitter3';
import ShortId from 'short-unique-id';
import { applyPatch } from 'fast-json-patch';
// ../../node_modules/.pnpm/fast-json-patch@3.1.1/node_modules/fast-json-patch/module/core.mjs
var core_exports = {};
__export(core_exports, {
JsonPatchError: () => JsonPatchError,
_areEquals: () => _areEquals,
applyOperation: () => applyOperation,
applyPatch: () => applyPatch,
applyReducer: () => applyReducer,
deepClone: () => deepClone,
getValueByPointer: () => getValueByPointer,
validate: () => validate,
validator: () => validator
});
// ../../node_modules/.pnpm/fast-json-patch@3.1.1/node_modules/fast-json-patch/module/helpers.mjs
var __extends = /* @__PURE__ */ (function() {
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf || { __proto__: [] } instanceof Array && function(d2, b2) {
d2.__proto__ = b2;
} || function(d2, b2) {
for (var p in b2) if (b2.hasOwnProperty(p)) d2[p] = b2[p];
};
return extendStatics(d, b);
};
return function(d, b) {
extendStatics(d, b);
function __() {
this.constructor = d;
}
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var _hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwnProperty(obj, key) {
return _hasOwnProperty.call(obj, key);
}
function _objectKeys(obj) {
if (Array.isArray(obj)) {
var keys_1 = new Array(obj.length);
for (var k = 0; k < keys_1.length; k++) {
keys_1[k] = "" + k;
}
return keys_1;
}
if (Object.keys) {
return Object.keys(obj);
}
var keys = [];
for (var i in obj) {
if (hasOwnProperty(obj, i)) {
keys.push(i);
}
}
return keys;
}
function _deepClone(obj) {
switch (typeof obj) {
case "object":
return JSON.parse(JSON.stringify(obj));
//Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5
case "undefined":
return null;
//this is how JSON.stringify behaves for array items
default:
return obj;
}
}
function isInteger(str) {
var i = 0;
var len = str.length;
var charCode;
while (i < len) {
charCode = str.charCodeAt(i);
if (charCode >= 48 && charCode <= 57) {
i++;
continue;
}
return false;
}
return true;
}
function escapePathComponent(path) {
if (path.indexOf("/") === -1 && path.indexOf("~") === -1)
return path;
return path.replace(/~/g, "~0").replace(/\//g, "~1");
}
function unescapePathComponent(path) {
return path.replace(/~1/g, "/").replace(/~0/g, "~");
}
function hasUndefined(obj) {
if (obj === void 0) {
return true;
}
if (obj) {
if (Array.isArray(obj)) {
for (var i_1 = 0, len = obj.length; i_1 < len; i_1++) {
if (hasUndefined(obj[i_1])) {
return true;
}
}
} else if (typeof obj === "object") {
var objKeys = _objectKeys(obj);
var objKeysLength = objKeys.length;
for (var i = 0; i < objKeysLength; i++) {
if (hasUndefined(obj[objKeys[i]])) {
return true;
}
}
}
}
return false;
}
function patchErrorMessageFormatter(message, args) {
var messageParts = [message];
for (var key in args) {
var value = typeof args[key] === "object" ? JSON.stringify(args[key], null, 2) : args[key];
if (typeof value !== "undefined") {
messageParts.push(key + ": " + value);
}
}
return messageParts.join("\n");
}
var PatchError = (
/** @class */
(function(_super) {
__extends(PatchError2, _super);
function PatchError2(message, name, index, operation, tree) {
var _newTarget = this.constructor;
var _this = _super.call(this, patchErrorMessageFormatter(message, { name, index, operation, tree })) || this;
_this.name = name;
_this.index = index;
_this.operation = operation;
_this.tree = tree;
Object.setPrototypeOf(_this, _newTarget.prototype);
_this.message = patchErrorMessageFormatter(message, { name, index, operation, tree });
return _this;
}
return PatchError2;
})(Error)
);
// ../../node_modules/.pnpm/fast-json-patch@3.1.1/node_modules/fast-json-patch/module/core.mjs
var JsonPatchError = PatchError;
var deepClone = _deepClone;
var objOps = {
add: function(obj, key, document) {
obj[key] = this.value;
return { newDocument: document };
},
remove: function(obj, key, document) {
var removed = obj[key];
delete obj[key];
return { newDocument: document, removed };
},
replace: function(obj, key, document) {
var removed = obj[key];
obj[key] = this.value;
return { newDocument: document, removed };
},
move: function(obj, key, document) {
var removed = getValueByPointer(document, this.path);
if (removed) {
removed = _deepClone(removed);
}
var originalValue = applyOperation(document, { op: "remove", path: this.from }).removed;
applyOperation(document, { op: "add", path: this.path, value: originalValue });
return { newDocument: document, removed };
},
copy: function(obj, key, document) {
var valueToCopy = getValueByPointer(document, this.from);
applyOperation(document, { op: "add", path: this.path, value: _deepClone(valueToCopy) });
return { newDocument: document };
},
test: function(obj, key, document) {
return { newDocument: document, test: _areEquals(obj[key], this.value) };
},
_get: function(obj, key, document) {
this.value = obj[key];
return { newDocument: document };
}
};
var arrOps = {
add: function(arr, i, document) {
if (isInteger(i)) {
arr.splice(i, 0, this.value);
} else {
arr[i] = this.value;
}
return { newDocument: document, index: i };
},
remove: function(arr, i, document) {
var removedList = arr.splice(i, 1);
return { newDocument: document, removed: removedList[0] };
},
replace: function(arr, i, document) {
var removed = arr[i];
arr[i] = this.value;
return { newDocument: document, removed };
},
move: objOps.move,
copy: objOps.copy,
test: objOps.test,
_get: objOps._get
};
function getValueByPointer(document, pointer) {
if (pointer == "") {
return document;
}
var getOriginalDestination = { op: "_get", path: pointer };
applyOperation(document, getOriginalDestination);
return getOriginalDestination.value;
}
function applyOperation(document, operation, validateOperation, mutateDocument, banPrototypeModifications, index) {
if (validateOperation === void 0) {
validateOperation = false;
}
if (mutateDocument === void 0) {
mutateDocument = true;
}
if (banPrototypeModifications === void 0) {
banPrototypeModifications = true;
}
if (index === void 0) {
index = 0;
}
if (validateOperation) {
if (typeof validateOperation == "function") {
validateOperation(operation, 0, document, operation.path);
} else {
validator(operation, 0);
}
}
if (operation.path === "") {
var returnValue = { newDocument: document };
if (operation.op === "add") {
returnValue.newDocument = operation.value;
return returnValue;
} else if (operation.op === "replace") {
returnValue.newDocument = operation.value;
returnValue.removed = document;
return returnValue;
} else if (operation.op === "move" || operation.op === "copy") {
returnValue.newDocument = getValueByPointer(document, operation.from);
if (operation.op === "move") {
returnValue.removed = document;
}
return returnValue;
} else if (operation.op === "test") {
returnValue.test = _areEquals(document, operation.value);
if (returnValue.test === false) {
throw new JsonPatchError("Test operation failed", "TEST_OPERATION_FAILED", index, operation, document);
}
returnValue.newDocument = document;
return returnValue;
} else if (operation.op === "remove") {
returnValue.removed = document;
returnValue.newDocument = null;
return returnValue;
} else if (operation.op === "_get") {
operation.value = document;
return returnValue;
} else {
if (validateOperation) {
throw new JsonPatchError("Operation `op` property is not one of operations defined in RFC-6902", "OPERATION_OP_INVALID", index, operation, document);
} else {
return returnValue;
}
}
} else {
if (!mutateDocument) {
document = _deepClone(document);
}
var path = operation.path || "";
var keys = path.split("/");
var obj = document;
var t = 1;
var len = keys.length;
var existingPathFragment = void 0;
var key = void 0;
var validateFunction = void 0;
if (typeof validateOperation == "function") {
validateFunction = validateOperation;
} else {
validateFunction = validator;
}
while (true) {
key = keys[t];
if (key && key.indexOf("~") != -1) {
key = unescapePathComponent(key);
}
if (banPrototypeModifications && (key == "__proto__" || key == "prototype" && t > 0 && keys[t - 1] == "constructor")) {
throw new TypeError("JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README");
}
if (validateOperation) {
if (existingPathFragment === void 0) {
if (obj[key] === void 0) {
existingPathFragment = keys.slice(0, t).join("/");
} else if (t == len - 1) {
existingPathFragment = operation.path;
}
if (existingPathFragment !== void 0) {
validateFunction(operation, 0, document, existingPathFragment);
}
}
}
t++;
if (Array.isArray(obj)) {
if (key === "-") {
key = obj.length;
} else {
if (validateOperation && !isInteger(key)) {
throw new JsonPatchError("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index", "OPERATION_PATH_ILLEGAL_ARRAY_INDEX", index, operation, document);
} else if (isInteger(key)) {
key = ~~key;
}
}
if (t >= len) {
if (validateOperation && operation.op === "add" && key > obj.length) {
throw new JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array", "OPERATION_VALUE_OUT_OF_BOUNDS", index, operation, document);
}
var returnValue = arrOps[operation.op].call(operation, obj, key, document);
if (returnValue.test === false) {
throw new JsonPatchError("Test operation failed", "TEST_OPERATION_FAILED", index, operation, document);
}
return returnValue;
}
} else {
if (t >= len) {
var returnValue = objOps[operation.op].call(operation, obj, key, document);
if (returnValue.test === false) {
throw new JsonPatchError("Test operation failed", "TEST_OPERATION_FAILED", index, operation, document);
}
return returnValue;
}
}
obj = obj[key];
if (validateOperation && t < len && (!obj || typeof obj !== "object")) {
throw new JsonPatchError("Cannot perform operation at the desired path", "OPERATION_PATH_UNRESOLVABLE", index, operation, document);
}
}
}
}
function applyPatch(document, patch, validateOperation, mutateDocument, banPrototypeModifications) {
if (mutateDocument === void 0) {
mutateDocument = true;
}
if (banPrototypeModifications === void 0) {
banPrototypeModifications = true;
}
if (validateOperation) {
if (!Array.isArray(patch)) {
throw new JsonPatchError("Patch sequence must be an array", "SEQUENCE_NOT_AN_ARRAY");
}
}
if (!mutateDocument) {
document = _deepClone(document);
}
var results = new Array(patch.length);
for (var i = 0, length_1 = patch.length; i < length_1; i++) {
results[i] = applyOperation(document, patch[i], validateOperation, true, banPrototypeModifications, i);
document = results[i].newDocument;
}
results.newDocument = document;
return results;
}
function applyReducer(document, operation, index) {
var operationResult = applyOperation(document, operation);
if (operationResult.test === false) {
throw new JsonPatchError("Test operation failed", "TEST_OPERATION_FAILED", index, operation, document);
}
return operationResult.newDocument;
}
function validator(operation, index, document, existingPathFragment) {
if (typeof operation !== "object" || operation === null || Array.isArray(operation)) {
throw new JsonPatchError("Operation is not an object", "OPERATION_NOT_AN_OBJECT", index, operation, document);
} else if (!objOps[operation.op]) {
throw new JsonPatchError("Operation `op` property is not one of operations defined in RFC-6902", "OPERATION_OP_INVALID", index, operation, document);
} else if (typeof operation.path !== "string") {
throw new JsonPatchError("Operation `path` property is not a string", "OPERATION_PATH_INVALID", index, operation, document);
} else if (operation.path.indexOf("/") !== 0 && operation.path.length > 0) {
throw new JsonPatchError('Operation `path` property must start with "/"', "OPERATION_PATH_INVALID", index, operation, document);
} else if ((operation.op === "move" || operation.op === "copy") && typeof operation.from !== "string") {
throw new JsonPatchError("Operation `from` property is not present (applicable in `move` and `copy` operations)", "OPERATION_FROM_REQUIRED", index, operation, document);
} else if ((operation.op === "add" || operation.op === "replace" || operation.op === "test") && operation.value === void 0) {
throw new JsonPatchError("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)", "OPERATION_VALUE_REQUIRED", index, operation, document);
} else if ((operation.op === "add" || operation.op === "replace" || operation.op === "test") && hasUndefined(operation.value)) {
throw new JsonPatchError("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)", "OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED", index, operation, document);
} else if (document) {
if (operation.op == "add") {
var pathLen = operation.path.split("/").length;
var existingPathLen = existingPathFragment.split("/").length;
if (pathLen !== existingPathLen + 1 && pathLen !== existingPathLen) {
throw new JsonPatchError("Cannot perform an `add` operation at the desired path", "OPERATION_PATH_CANNOT_ADD", index, operation, document);
}
} else if (operation.op === "replace" || operation.op === "remove" || operation.op === "_get") {
if (operation.path !== existingPathFragment) {
throw new JsonPatchError("Cannot perform the operation at a path that does not exist", "OPERATION_PATH_UNRESOLVABLE", index, operation, document);
}
} else if (operation.op === "move" || operation.op === "copy") {
var existingValue = { op: "_get", path: operation.from, value: void 0 };
var error = validate([existingValue], document);
if (error && error.name === "OPERATION_PATH_UNRESOLVABLE") {
throw new JsonPatchError("Cannot perform the operation from a path that does not exist", "OPERATION_FROM_UNRESOLVABLE", index, operation, document);
}
}
}
}
function validate(sequence, document, externalValidator) {
try {
if (!Array.isArray(sequence)) {
throw new JsonPatchError("Patch sequence must be an array", "SEQUENCE_NOT_AN_ARRAY");
}
if (document) {
applyPatch(_deepClone(document), _deepClone(sequence), externalValidator || true);
} else {
externalValidator = externalValidator || validator;
for (var i = 0; i < sequence.length; i++) {
externalValidator(sequence[i], i, document, void 0);
}
}
} catch (e) {
if (e instanceof JsonPatchError) {
return e;
} else {
throw e;
}
}
}
function _areEquals(a, b) {
if (a === b)
return true;
if (a && b && typeof a == "object" && typeof b == "object") {
var arrA = Array.isArray(a), arrB = Array.isArray(b), i, length, key;
if (arrA && arrB) {
length = a.length;
if (length != b.length)
return false;
for (i = length; i-- !== 0; )
if (!_areEquals(a[i], b[i]))
return false;
return true;
}
if (arrA != arrB)
return false;
var keys = Object.keys(a);
length = keys.length;
if (length !== Object.keys(b).length)
return false;
for (i = length; i-- !== 0; )
if (!b.hasOwnProperty(keys[i]))
return false;
for (i = length; i-- !== 0; ) {
key = keys[i];
if (!_areEquals(a[key], b[key]))
return false;
}
return true;
}
return a !== a && b !== b;
}
// ../../node_modules/.pnpm/fast-json-patch@3.1.1/node_modules/fast-json-patch/module/duplex.mjs
var duplex_exports = {};
__export(duplex_exports, {
compare: () => compare,
generate: () => generate,
observe: () => observe,
unobserve: () => unobserve
});
var beforeDict = /* @__PURE__ */ new WeakMap();
var Mirror = (
/** @class */
/* @__PURE__ */ (function() {
function Mirror2(obj) {
this.observers = /* @__PURE__ */ new Map();
this.obj = obj;
}
return Mirror2;
})()
);
var ObserverInfo = (
/** @class */
/* @__PURE__ */ (function() {
function ObserverInfo2(callback, observer) {
this.callback = callback;
this.observer = observer;
}
return ObserverInfo2;
})()
);
function getMirror(obj) {
return beforeDict.get(obj);
}
function getObserverFromMirror(mirror, callback) {
return mirror.observers.get(callback);
}
function removeObserverFromMirror(mirror, observer) {
mirror.observers.delete(observer.callback);
}
function unobserve(root, observer) {
observer.unobserve();
}
function observe(obj, callback) {
var patches = [];
var observer;
var mirror = getMirror(obj);
if (!mirror) {
mirror = new Mirror(obj);
beforeDict.set(obj, mirror);
} else {
var observerInfo = getObserverFromMirror(mirror, callback);
observer = observerInfo && observerInfo.observer;
}
if (observer) {
return observer;
}
observer = {};
mirror.value = _deepClone(obj);
if (callback) {
observer.callback = callback;
observer.next = null;
var dirtyCheck = function() {
generate(observer);
};
var fastCheck = function() {
clearTimeout(observer.next);
observer.next = setTimeout(dirtyCheck);
};
if (typeof window !== "undefined") {
window.addEventListener("mouseup", fastCheck);
window.addEventListener("keyup", fastCheck);
window.addEventListener("mousedown", fastCheck);
window.addEventListener("keydown", fastCheck);
window.addEventListener("change", fastCheck);
}
}
observer.patches = patches;
observer.object = obj;
observer.unobserve = function() {
generate(observer);
clearTimeout(observer.next);
removeObserverFromMirror(mirror, observer);
if (typeof window !== "undefined") {
window.removeEventListener("mouseup", fastCheck);
window.removeEventListener("keyup", fastCheck);
window.removeEventListener("mousedown", fastCheck);
window.removeEventListener("keydown", fastCheck);
window.removeEventListener("change", fastCheck);
}
};
mirror.observers.set(callback, new ObserverInfo(callback, observer));
return observer;
}
function generate(observer, invertible) {
if (invertible === void 0) {
invertible = false;
}
var mirror = beforeDict.get(observer.object);
_generate(mirror.value, observer.object, observer.patches, "", invertible);
if (observer.patches.length) {
applyPatch(mirror.value, observer.patches);
}
var temp = observer.patches;
if (temp.length > 0) {
observer.patches = [];
if (observer.callback) {
observer.callback(temp);
}
}
return temp;
}
function _generate(mirror, obj, patches, path, invertible) {
if (obj === mirror) {
return;
}
if (typeof obj.toJSON === "function") {
obj = obj.toJSON();
}
var newKeys = _objectKeys(obj);
var oldKeys = _objectKeys(mirror);
var deleted = false;
for (var t = oldKeys.length - 1; t >= 0; t--) {
var key = oldKeys[t];
var oldVal = mirror[key];
if (hasOwnProperty(obj, key) && !(obj[key] === void 0 && oldVal !== void 0 && Array.isArray(obj) === false)) {
var newVal = obj[key];
if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null && Array.isArray(oldVal) === Array.isArray(newVal)) {
_generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key), invertible);
} else {
if (oldVal !== newVal) {
if (invertible) {
patches.push({ op: "test", path: path + "/" + escapePathComponent(key), value: _deepClone(oldVal) });
}
patches.push({ op: "replace", path: path + "/" + escapePathComponent(key), value: _deepClone(newVal) });
}
}
} else if (Array.isArray(mirror) === Array.isArray(obj)) {
if (invertible) {
patches.push({ op: "test", path: path + "/" + escapePathComponent(key), value: _deepClone(oldVal) });
}
patches.push({ op: "remove", path: path + "/" + escapePathComponent(key) });
deleted = true;
} else {
if (invertible) {
patches.push({ op: "test", path, value: mirror });
}
patches.push({ op: "replace", path, value: obj });
}
}
if (!deleted && newKeys.length == oldKeys.length) {
return;
}
for (var t = 0; t < newKeys.length; t++) {
var key = newKeys[t];
if (!hasOwnProperty(mirror, key) && obj[key] !== void 0) {
patches.push({ op: "add", path: path + "/" + escapePathComponent(key), value: _deepClone(obj[key]) });
}
}
}
function compare(tree1, tree2, invertible) {
if (invertible === void 0) {
invertible = false;
}
var patches = [];
_generate(tree1, tree2, patches, "", invertible);
return patches;
}
// ../../node_modules/.pnpm/fast-json-patch@3.1.1/node_modules/fast-json-patch/index.mjs
Object.assign({}, core_exports, duplex_exports, {
JsonPatchError: PatchError,
deepClone: _deepClone,
escapePathComponent,
unescapePathComponent
});
// src/Model.ts
var uid = new ShortId({ length: 20 });

@@ -18,2 +656,3 @@ function generateId() {

var SYM_INIT_DATA = /* @__PURE__ */ Symbol("parcae:initData");
var SYM_SERVER_MERGE = /* @__PURE__ */ Symbol("parcae:serverMerge");
var INSTANCE_METHODS = /* @__PURE__ */ new Set([

@@ -32,52 +671,6 @@ "save",

function lazyQuery(modelClass, steps = []) {
const CHAINABLE = [
"select",
"where",
"andWhere",
"orWhere",
"whereIn",
"whereNot",
"whereNotIn",
"whereNull",
"whereNotNull",
"whereBetween",
"whereRaw",
"orWhereRaw",
"orWhereIn",
"orWhereNull",
"whereExists",
"orderBy",
"orderByRaw",
"groupBy",
"groupByRaw",
"having",
"havingRaw",
"limit",
"offset",
"distinct",
"distinctOn",
"join",
"innerJoin",
"leftJoin",
"rightJoin",
"clearOrder",
"clearSelect",
"from",
"sum",
"avg",
"min",
"max",
"increment",
"decrement"
];
const chain = {};
for (const method of CHAINABLE) {
for (const method of CHAINABLE_METHODS) {
chain[method] = (...args) => lazyQuery(modelClass, [...steps, { method, args }]);
}
chain.basic = (limit = 25, sort = "createdAt", direction = "desc", page = 0) => lazyQuery(modelClass, [
...steps,
{ method: "orderBy", args: [sort, direction] },
{ method: "limit", args: [limit] },
{ method: "offset", args: [page * limit] }
]);
const resolve = async () => {

@@ -92,3 +685,2 @@ const adapter = Model.hasAdapter() ? Model.getAdapter() : await Model.waitForAdapter();

chain.find = async () => (await resolve()).find();
chain.findAndSubscribe = async () => (await resolve()).findAndSubscribe();
chain.first = async () => (await resolve()).first();

@@ -123,2 +715,3 @@ chain.count = async () => (await resolve()).count();

static indexes;
static searchFields;
static managed = true;

@@ -218,4 +811,4 @@ /** @internal */

}
static basic(limit, sort, direction, page) {
return this._query().basic(limit, sort, direction, page);
static search(term) {
return this._query().search(term);
}

@@ -280,2 +873,23 @@ // ── Constructor ────────────────────────────────────────────────────

get(target2, prop) {
if (prop === SYM_SERVER_MERGE) {
return (serverData) => {
const pendingUpdates = new Set(target2[SYM_UPDATES]);
const pendingPatches = target2[SYM_PATCHING];
const serverKeys = new Set(Object.keys(serverData));
for (const key of serverKeys) {
if (pendingUpdates.has(key) || pendingPatches.has(key)) continue;
target2[key] = serverData[key];
}
for (const key of Object.keys(target2)) {
if (SYSTEM_DATA_KEYS.has(key)) continue;
if (INSTANCE_METHODS.has(key)) continue;
if (EVENTEMITTER_KEYS.has(key)) continue;
if (key.startsWith("_")) continue;
if (serverKeys.has(key)) continue;
if (pendingUpdates.has(key) || pendingPatches.has(key)) continue;
delete target2[key];
}
return new Proxy(target2, this);
};
}
if (typeof prop === "symbol") {

@@ -447,13 +1061,8 @@ return target2[prop];

const freshData = fresh.__data;
const pendingKeys = this.__pendingKeys;
for (const key of Object.keys(freshData)) {
if (!pendingKeys.has(key)) {
this[key] = freshData[key];
}
const merge = this[SYM_SERVER_MERGE];
if (merge) {
merge(freshData);
}
}
}
async load() {
return this.refresh();
}
async sanitize(_user) {

@@ -467,9 +1076,12 @@ return { type: this.type, ...this.__data };

static __refCache = /* @__PURE__ */ new Map();
static REF_CACHE_TTL = 3e4;
// 30 seconds
_createRefProxy(targetClass, refId) {
const cacheKey = `${targetClass.type}:${refId}`;
const cached = _Model.__refCache.get(cacheKey);
if (cached) return cached;
if (cached && cached.expires > Date.now()) return cached.value;
if (cached) _Model.__refCache.delete(cacheKey);
let loaded = null;
let loading = null;
return new Proxy({}, {
const proxy = new Proxy({}, {
get(_target, prop) {

@@ -486,3 +1098,6 @@ if (prop === "id") return refId;

loaded = result;
_Model.__refCache.set(cacheKey, loaded);
_Model.__refCache.set(cacheKey, {
value: proxy,
expires: Date.now() + _Model.REF_CACHE_TTL
});
return result;

@@ -494,7 +1109,24 @@ });

});
return proxy;
}
};
/*! Bundled license information:
export { Model, generateId };
fast-json-patch/module/helpers.mjs:
(*!
* https://github.com/Starcounter-Jack/JSON-Patch
* (c) 2017-2022 Joachim Wester
* MIT licensed
*)
fast-json-patch/module/duplex.mjs:
(*!
* https://github.com/Starcounter-Jack/JSON-Patch
* (c) 2017-2021 Joachim Wester
* MIT license
*)
*/
export { Model, SYM_SERVER_MERGE, generateId };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map
{
"name": "@parcae/model",
"version": "0.7.0",
"version": "0.8.0",
"description": "Parcae Model — typed ORM base class with adapter pattern",

@@ -44,3 +44,2 @@ "type": "module",

"build": "tsup",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit",

@@ -47,0 +46,0 @@ "clean": "rm -rf dist",

+37
-37

@@ -19,8 +19,8 @@ # @parcae/model

user!: User; // reference -> VARCHAR storing ID
title: string = ""; // string -> VARCHAR
body: PostBody = {}; // object -> JSONB
tags: string[] = []; // array -> JSONB
user!: User; // reference -> VARCHAR storing ID
title: string = ""; // string -> VARCHAR
body: PostBody = {}; // object -> JSONB
tags: string[] = []; // array -> JSONB
published: boolean = false; // boolean -> BOOLEAN
views: number = 0; // number -> DOUBLE PRECISION
views: number = 0; // number -> DOUBLE PRECISION
}

@@ -38,7 +38,7 @@ ```

post.title; // "Hello" — reads from data store
post.title = "Updated"; // change tracked automatically
post.published; // false — typed as boolean
post.title; // "Hello" — reads from data store
post.title = "Updated"; // change tracked automatically
post.published; // false — typed as boolean
await post.save(); // flushes tracked changes
await post.save(); // flushes tracked changes
```

@@ -51,8 +51,8 @@

```typescript
post.user; // User proxy — loads on property access, Suspense-compatible
post.$user; // "user_k8f2m9x" — raw string ID, no loading
post.user; // User proxy — loads on property access, Suspense-compatible
post.$user; // "user_k8f2m9x" — raw string ID, no loading
// Setting a reference accepts a Model instance or raw ID
post.user = someUser; // extracts someUser.id
post.$user = "user_abc123"; // sets raw ID directly
post.user = someUser; // extracts someUser.id
post.$user = "user_abc123"; // sets raw ID directly
```

@@ -99,3 +99,3 @@

const post = Post.create({ title: "New Post" });
post.id; // auto-generated 20-char ID
post.id; // auto-generated 20-char ID

@@ -108,3 +108,3 @@ // Save (insert or update)

post.title = "A";
post.title = "AB"; // batched into single save
post.title = "AB"; // batched into single save
await post.save();

@@ -125,3 +125,3 @@

// Serialize
post.toJSON(); // { type, id, title, ... }
post.toJSON(); // { type, id, title, ... }
await post.sanitize(user); // override in subclass to strip fields

@@ -163,6 +163,6 @@ ```

| Adapter | Store | Persistence |
| --- | --- | --- |
| Adapter | Store | Persistence |
| ----------------- | ----------------------- | ------------------------------- |
| `FrontendAdapter` | Valtio proxy (reactive) | Transport RPC (Socket.IO / SSE) |
| `BackendAdapter` | Plain object | Knex + PostgreSQL |
| `BackendAdapter` | Plain object | Knex + PostgreSQL |

@@ -211,23 +211,23 @@ Set the adapter once at startup:

| Property | Type | Description |
| --- | --- | --- |
| `type` | `string` | Model identifier. Used for table naming and routing. |
| `path` | `string?` | Custom API path. Defaults to `/v1/{type}s`. |
| `scope` | `ModelScope?` | Row-level security rules. |
| `indexes` | `IndexDefinition[]?` | Database index definitions. |
| `managed` | `boolean` | `false` for externally managed tables (e.g. auth). Default: `true`. |
| `__schema` | `SchemaDefinition?` | Resolved at startup by RTTIST. Maps properties to column types. |
| Property | Type | Description |
| ---------- | -------------------- | ------------------------------------------------------------------- |
| `type` | `string` | Model identifier. Used for table naming and routing. |
| `path` | `string?` | Custom API path. Defaults to `/v1/{type}s`. |
| `scope` | `ModelScope?` | Row-level security rules. |
| `indexes` | `IndexDefinition[]?` | Database index definitions. |
| `managed` | `boolean` | `false` for externally managed tables (e.g. auth). Default: `true`. |
| `__schema` | `SchemaDefinition?` | Resolved at startup by RTTIST. Maps properties to column types. |
## Type Mapping
| TypeScript | ColumnType | Postgres |
| --- | --- | --- |
| `string` | `"string"` | VARCHAR(2048) |
| `string` (long) | `"text"` | TEXT |
| `number` (int) | `"integer"` | INTEGER |
| `number` (float) | `"number"` | DOUBLE PRECISION |
| `boolean` | `"boolean"` | BOOLEAN |
| `Date` | `"datetime"` | TIMESTAMP |
| `SomeModel` | `{ kind: "ref" }` | VARCHAR (foreign key) |
| object / array | `"json"` | JSONB |
| TypeScript | ColumnType | Postgres |
| ---------------- | ----------------- | --------------------- |
| `string` | `"string"` | VARCHAR(2048) |
| `string` (long) | `"text"` | TEXT |
| `number` (int) | `"integer"` | INTEGER |
| `number` (float) | `"number"` | DOUBLE PRECISION |
| `boolean` | `"boolean"` | BOOLEAN |
| `Date` | `"datetime"` | TIMESTAMP |
| `SomeModel` | `{ kind: "ref" }` | VARCHAR (foreign key) |
| object / array | `"json"` | JSONB |

@@ -234,0 +234,0 @@ ## Exports

import { proxy } from 'valtio';
// src/adapters/client.ts
function stripVersion(path) {
return path.replace(/^\/v\d+/, "");
}
var FrontendAdapter = class {
transport;
constructor(transport) {
this.transport = transport;
}
p(modelPath) {
return stripVersion(modelPath);
}
// ── createStore ──────────────────────────────────────────────────────
createStore(data) {
return proxy(data);
}
// ── save ─────────────────────────────────────────────────────────────
async save(model, changes) {
const ModelClass = model.constructor;
const path = this.resolvePath(ModelClass);
if (changes.creating) {
const result = await this.transport.post(path, model.__data);
if (result?.id) model.__data.id = result.id;
} else if (changes.ops.length > 0) {
await this.transport.patch(`${path}/${model.id}`, { ops: changes.ops });
} else if (changes.updates.length > 0) {
const data = {};
for (const key of changes.updates) {
data[key] = model.__data[key];
}
await this.transport.put(`${path}/${model.id}`, data);
}
}
// ── remove ───────────────────────────────────────────────────────────
async remove(model) {
const ModelClass = model.constructor;
const path = this.resolvePath(ModelClass);
await this.transport.delete(`${path}/${model.id}`);
}
// ── findById ─────────────────────────────────────────────────────────
async findById(modelClass, id) {
const path = this.resolvePath(modelClass);
try {
const data = await this.transport.get(`${path}/${id}`);
if (!data) return null;
return new modelClass(this, data);
} catch {
return null;
}
}
// ── query ────────────────────────────────────────────────────────────
query(modelClass) {
return this._buildQuery(modelClass, []);
}
_buildQuery(modelClass, steps) {
const adapter = this;
const CHAINABLE = [
"select",
"where",
"andWhere",
"orWhere",
"whereIn",
"whereNot",
"whereNotIn",
"whereNull",
"whereNotNull",
"whereBetween",
"whereRaw",
"orWhereRaw",
"orWhereIn",
"orWhereNull",
"whereExists",
"orderBy",
"orderByRaw",
"groupBy",
"groupByRaw",
"having",
"havingRaw",
"limit",
"offset",
"distinct",
"distinctOn",
"join",
"innerJoin",
"leftJoin",
"rightJoin",
"clearOrder",
"clearSelect",
"from",
"sum",
"avg",
"min",
"max",
"increment",
"decrement"
];
const chain = {};
for (const method of CHAINABLE) {
chain[method] = (...args) => {
return adapter._buildQuery(modelClass, [
...steps,
{ method, args: adapter._serializeArgs(args) }
]);
};
}
chain.basic = (limit = 25, sort = "createdAt", direction = "desc", page = 0) => {
return adapter._buildQuery(modelClass, [
...steps,
{ method: "orderBy", args: [sort, direction] },
{ method: "limit", args: [limit] },
{ method: "offset", args: [page * limit] }
]);
};
let debounceMs = void 0;
chain.debounce = (ms) => {
debounceMs = ms;
const newChain = adapter._buildQuery(modelClass, steps);
newChain.__debounceMs = ms;
return newChain;
};
chain.find = async () => {
const path = adapter.resolvePath(modelClass);
const result = await adapter.transport.get(path, { __query: steps });
const items = result?.[modelClass.type + "s"] ?? result?.items ?? [];
return items.map((row) => new modelClass(adapter, row));
};
chain.findAndSubscribe = async () => {
const path = adapter.resolvePath(modelClass);
const result = await adapter.transport.get(path, {
__query: steps,
__subscribe: true
});
const items = result?.[modelClass.type + "s"] ?? result?.items ?? [];
return {
items: items.map((row) => new modelClass(adapter, row)),
hash: result?.__hash ?? null
};
};
chain.first = async () => {
const all = await chain.find();
return all[0] ?? null;
};
chain.count = async () => {
const path = adapter.resolvePath(modelClass);
const result = await adapter.transport.get(path, {
__query: steps,
__count: true
});
return result?.total ?? 0;
};
chain.__steps = steps;
chain.__modelType = modelClass.type;
chain.__modelClass = modelClass;
chain.__adapter = adapter;
Object.defineProperty(chain, "__debounceMs", {
get: () => debounceMs,
enumerable: true
});
return chain;
}
_serializeArgs(args) {
return args.map((arg) => {
if (typeof arg === "function") return "__fn__";
return arg;
});
}
// ── patch ────────────────────────────────────────────────────────────
async patch(model, ops) {
const ModelClass = model.constructor;
const path = this.resolvePath(ModelClass);
await this.transport.patch(`${path}/${model.id}`, { ops });
}
// ── helpers ──────────────────────────────────────────────────────────
resolvePath(modelClass) {
if (!modelClass.path && !modelClass.type) {
throw new Error(`Model has no path or type`);
}
const fullPath = modelClass.path ?? `/v1/${modelClass.type}s`;
return this.p(fullPath);
}
};
var client_default = FrontendAdapter;
export { FrontendAdapter, client_default };
//# sourceMappingURL=chunk-UE7DW2FG.js.map
//# sourceMappingURL=chunk-UE7DW2FG.js.map
{"version":3,"sources":["../src/adapters/client.ts"],"names":[],"mappings":"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","file":"chunk-UE7DW2FG.js","sourcesContent":["/**\n * FrontendAdapter — Valtio + Transport for Parcae Model.\n *\n * Transport-agnostic. Works with any implementation of the Transport interface:\n * - SocketTransport (Socket.IO — bidirectional, realtime)\n * - SSETransport (Server-Sent Events — read-heavy, simpler)\n * - GRPCTransport (gRPC — service-to-service)\n * - Or any custom transport\n */\n\nimport { proxy } from \"valtio\";\nimport type {\n ModelAdapter,\n ModelConstructor,\n ChangeSet,\n QueryChain,\n QueryStep,\n PatchOp,\n} from \"./types\";\n\n// ─── Transport Interface ─────────────────────────────────────────────────────\n\n/**\n * Abstract transport layer. Decouples the Model system from the wire protocol.\n *\n * Implementations:\n * - SocketTransport: Socket.IO (bidirectional, realtime subscriptions)\n * - SSETransport: HTTP + Server-Sent Events (read-heavy, simpler infra)\n * - GRPCTransport: gRPC (service-to-service, protobuf)\n */\nexport interface Transport {\n // ── Request/Response (RPC-style) ────────────────────────────────────\n get(path: string, data?: any): Promise<any>;\n post(path: string, data?: any): Promise<any>;\n put(path: string, data?: any): Promise<any>;\n patch(path: string, data?: any): Promise<any>;\n delete(path: string, data?: any): Promise<any>;\n\n // ── Subscriptions (realtime) ────────────────────────────────────────\n /**\n * Subscribe to a named event stream.\n * Returns a dispose function to unsubscribe.\n *\n * Socket.IO: socket.on(event, handler)\n * SSE: new EventSource(url) per channel\n * gRPC: server streaming RPC\n */\n subscribe?(event: string, handler: (...args: any[]) => void): () => void;\n\n /**\n * Unsubscribe from a named event stream.\n * Alternative to calling the dispose function returned by subscribe().\n */\n unsubscribe?(event: string, handler?: (...args: any[]) => void): void;\n\n // ── Control messages ────────────────────────────────────────────────\n /**\n * Send a control message to the server (fire-and-forget).\n *\n * Socket.IO: socket.emit(event, ...args)\n * SSE: POST /v1/__control { event, args }\n * gRPC: unary RPC\n */\n send?(event: string, ...args: any[]): void;\n\n // ── Connection state ────────────────────────────────────────────────\n /** Whether the transport is currently connected. */\n readonly isConnected?: boolean;\n /** Whether the transport is currently loading/connecting. */\n readonly isLoading?: boolean;\n\n /**\n * Listen for connection state changes.\n * Events: \"connected\", \"disconnected\", \"reconnecting\", \"error\"\n */\n on?(event: string, handler: (...args: any[]) => void): void;\n off?(event: string, handler?: (...args: any[]) => void): void;\n\n /** Disconnect the transport. */\n disconnect?(): void;\n /** Reconnect the transport. */\n reconnect?(): Promise<void>;\n}\n\n/**\n * Strip version prefix from a path if it starts with /v{number}.\n * The transport prepends the version automatically.\n */\nfunction stripVersion(path: string): string {\n return path.replace(/^\\/v\\d+/, \"\");\n}\n\n// ─── FrontendAdapter ─────────────────────────────────────────────────────────\n\nexport class FrontendAdapter implements ModelAdapter {\n public transport: Transport;\n\n constructor(transport: Transport) {\n this.transport = transport;\n }\n\n private p(modelPath: string): string {\n return stripVersion(modelPath);\n }\n\n // ── createStore ──────────────────────────────────────────────────────\n\n createStore(data: Record<string, any>): Record<string, any> {\n return proxy(data);\n }\n\n // ── save ─────────────────────────────────────────────────────────────\n\n async save(model: any, changes: ChangeSet): Promise<void> {\n const ModelClass = model.constructor as ModelConstructor;\n const path = this.resolvePath(ModelClass);\n\n if (changes.creating) {\n const result = await this.transport.post(path, model.__data);\n if (result?.id) model.__data.id = result.id;\n } else if (changes.ops.length > 0) {\n await this.transport.patch(`${path}/${model.id}`, { ops: changes.ops });\n } else if (changes.updates.length > 0) {\n const data: Record<string, any> = {};\n for (const key of changes.updates) {\n data[key] = model.__data[key];\n }\n await this.transport.put(`${path}/${model.id}`, data);\n }\n }\n\n // ── remove ───────────────────────────────────────────────────────────\n\n async remove(model: any): Promise<void> {\n const ModelClass = model.constructor as ModelConstructor;\n const path = this.resolvePath(ModelClass);\n await this.transport.delete(`${path}/${model.id}`);\n }\n\n // ── findById ─────────────────────────────────────────────────────────\n\n async findById<T>(\n modelClass: ModelConstructor<T>,\n id: string,\n ): Promise<T | null> {\n const path = this.resolvePath(modelClass);\n try {\n const data = await this.transport.get(`${path}/${id}`);\n if (!data) return null;\n return new modelClass(this, data) as T;\n } catch {\n return null;\n }\n }\n\n // ── query ────────────────────────────────────────────────────────────\n\n query<T>(modelClass: ModelConstructor<T>): QueryChain<T> {\n return this._buildQuery(modelClass, []);\n }\n\n private _buildQuery<T>(\n modelClass: ModelConstructor<T>,\n steps: QueryStep[],\n ): QueryChain<T> {\n const adapter = this;\n\n const CHAINABLE = [\n \"select\",\n \"where\",\n \"andWhere\",\n \"orWhere\",\n \"whereIn\",\n \"whereNot\",\n \"whereNotIn\",\n \"whereNull\",\n \"whereNotNull\",\n \"whereBetween\",\n \"whereRaw\",\n \"orWhereRaw\",\n \"orWhereIn\",\n \"orWhereNull\",\n \"whereExists\",\n \"orderBy\",\n \"orderByRaw\",\n \"groupBy\",\n \"groupByRaw\",\n \"having\",\n \"havingRaw\",\n \"limit\",\n \"offset\",\n \"distinct\",\n \"distinctOn\",\n \"join\",\n \"innerJoin\",\n \"leftJoin\",\n \"rightJoin\",\n \"clearOrder\",\n \"clearSelect\",\n \"from\",\n \"sum\",\n \"avg\",\n \"min\",\n \"max\",\n \"increment\",\n \"decrement\",\n ] as const;\n\n const chain: any = {};\n\n for (const method of CHAINABLE) {\n chain[method] = (...args: any[]) => {\n return adapter._buildQuery(modelClass, [\n ...steps,\n { method, args: adapter._serializeArgs(args) },\n ]);\n };\n }\n\n chain.basic = (\n limit: number = 25,\n sort: string = \"createdAt\",\n direction: \"asc\" | \"desc\" = \"desc\",\n page: number = 0,\n ) => {\n return adapter._buildQuery(modelClass, [\n ...steps,\n { method: \"orderBy\", args: [sort, direction] },\n { method: \"limit\", args: [limit] },\n { method: \"offset\", args: [page * limit] },\n ]);\n };\n\n let debounceMs: number | undefined = undefined;\n chain.debounce = (ms: number) => {\n debounceMs = ms;\n const newChain = adapter._buildQuery(modelClass, steps);\n (newChain as any).__debounceMs = ms;\n return newChain;\n };\n\n chain.find = async (): Promise<T[]> => {\n const path = adapter.resolvePath(modelClass);\n const result = await adapter.transport.get(path, { __query: steps });\n const items = result?.[modelClass.type + \"s\"] ?? result?.items ?? [];\n return items.map((row: any) => new modelClass(adapter, row));\n };\n\n /**\n * Like find(), but also subscribes to realtime updates.\n * Returns { items, hash } — the hash identifies the subscription.\n */\n chain.findAndSubscribe = async (): Promise<{\n items: T[];\n hash: string | null;\n }> => {\n const path = adapter.resolvePath(modelClass);\n const result = await adapter.transport.get(path, {\n __query: steps,\n __subscribe: true,\n });\n const items = result?.[modelClass.type + \"s\"] ?? result?.items ?? [];\n return {\n items: items.map((row: any) => new modelClass(adapter, row)),\n hash: result?.__hash ?? null,\n };\n };\n\n chain.first = async (): Promise<T | null> => {\n const all = await chain.find();\n return all[0] ?? null;\n };\n\n chain.count = async (): Promise<number> => {\n const path = adapter.resolvePath(modelClass);\n const result = await adapter.transport.get(path, {\n __query: steps,\n __count: true,\n });\n return result?.total ?? 0;\n };\n\n chain.__steps = steps;\n chain.__modelType = modelClass.type;\n chain.__modelClass = modelClass;\n chain.__adapter = adapter;\n Object.defineProperty(chain, \"__debounceMs\", {\n get: () => debounceMs,\n enumerable: true,\n });\n\n return chain as QueryChain<T>;\n }\n\n private _serializeArgs(args: any[]): any[] {\n return args.map((arg) => {\n if (typeof arg === \"function\") return \"__fn__\";\n return arg;\n });\n }\n\n // ── patch ────────────────────────────────────────────────────────────\n\n async patch(model: any, ops: PatchOp[]): Promise<void> {\n const ModelClass = model.constructor as ModelConstructor;\n const path = this.resolvePath(ModelClass);\n await this.transport.patch(`${path}/${model.id}`, { ops });\n }\n\n // ── helpers ──────────────────────────────────────────────────────────\n\n private resolvePath(modelClass: ModelConstructor): string {\n if (!modelClass.path && !modelClass.type) {\n throw new Error(`Model has no path or type`);\n }\n const fullPath = modelClass.path ?? `/v1/${modelClass.type}s`;\n return this.p(fullPath);\n }\n}\n\nexport default FrontendAdapter;\n"]}

Sorry, the diff of this file is too big to display