Spaces:
Running
Running
File size: 16,560 Bytes
aa89cb5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 | import {
CheckException, EntityChecker, RuntimeModule,
SelectQuery as RuntimeSelectQuery, UserContext,
executeRelationFacets, QueryRequest, MutationIntent, MutationRequest, GraphMutationSession,
} from "teaql-ts";
export { CheckException, EntityRoot, ObjectLocation, SmartList, UserContext, executeRelationFacets, MutationIntent } from "teaql-ts";
export { GraphMutationSession, LoadedScalarSnapshot, MutationTraceScope } from "teaql-ts";
export type { EntityKey } from "teaql-ts";
export type { TeaQLPage } from "teaql-ts";
import type { QueryOperation } from "teaql-ts";
function soundex(input: unknown): string {
const text = String(input ?? "").toUpperCase().replace(/[^A-Z]/g, "");
if (!text) return "";
const code = (char: string): string => "BFPV".includes(char) ? "1"
: "CGJKQSXZ".includes(char) ? "2" : "DT".includes(char) ? "3"
: char === "L" ? "4" : "MN".includes(char) ? "5" : char === "R" ? "6" : "";
let result = text[0], previous = code(text[0]);
for (const char of text.slice(1)) {
const current = code(char);
if (current && current !== previous) result += current;
previous = current;
if (result.length === 4) break;
}
return (result + "000").slice(0, 4);
}
/** Generated naming adapter; query policy and execution remain in the formal runtime. */
export class SelectQuery extends RuntimeSelectQuery {
orderBy(field: string, direction: string): this {
return this.order({ field, expr: null, direction: direction === "desc" ? "Desc" : "Asc" } as any);
}
relationQuery(
name: string, query: RuntimeSelectQuery, localKey = "id", foreignKey = "id", many = true,
operation?: QueryOperation,
): this {
return super.relationQuery(name, query, localKey, foreignKey, many, operation);
}
}
export interface TeaQLDataService {
executeGraphSave<T>(intent: MutationIntent, work: (graph: GraphMutationSession) => Promise<T>): Promise<T>;
preflightMutation(mutation: any): any;
afterGraphCommit(work: () => void): void;
afterGraphRollback(work: () => void): void;
executeMutation(mutation: any): Promise<any>;
executeQuery(query: any): Promise<any[]>;
executeCount(query: any, alias?: string, operation?: QueryOperation): Promise<number>;
executeForStream(query: any, chunkSize?: number): AsyncIterable<any[]>;
close?(): Promise<void>;
}
export class TeaQLClient implements TeaQLDataService {
private data: Record<string, Record<string, any>> = {};
private nextIds: Record<string, number> = {};
private readonly checkers: Record<string, EntityChecker> = {};
private userContext = new UserContext();
private graphSaveActive = false;
private graphCommitActions: Array<() => void> = [];
private graphRollbackActions: Array<() => void> = [];
private graphSaveTail: Promise<void> = Promise.resolve();
private readonly mutationGovernanceEvents: any[] = [];
constructor(private storagePath?: string) {
if (storagePath) {
const fs = require("fs");
if (fs.existsSync(storagePath)) {
const state = JSON.parse(fs.readFileSync(storagePath, "utf8"));
this.data = state.data || {};
this.nextIds = state.nextIds || {};
}
}
}
/** Installs passive generated metadata. It never mutates storage. */
install(module: RuntimeModule): this {
Object.assign(this.checkers, module.checkers);
return this;
}
setUserContext(context: UserContext): this {
this.userContext = context;
return this;
}
get mutationGovernanceTrace(): readonly any[] {
return [...this.mutationGovernanceEvents];
}
private persist() {
if (!this.storagePath) return;
const fs = require("fs");
const path = require("path");
fs.mkdirSync(path.dirname(path.resolve(this.storagePath)), { recursive: true });
const temporaryPath = `${this.storagePath}.tmp`;
fs.writeFileSync(temporaryPath, JSON.stringify({ data: this.data, nextIds: this.nextIds }));
fs.renameSync(temporaryPath, this.storagePath);
}
async executeGraphSave<T>(intent: MutationIntent, work: (graph: GraphMutationSession) => Promise<T>): Promise<T> {
const graph = new GraphMutationSession(intent);
const predecessor = this.graphSaveTail;
let release!: () => void;
this.graphSaveTail = new Promise<void>(resolve => { release = resolve; });
await predecessor;
const dataSnapshot = JSON.parse(JSON.stringify(this.data));
const nextIdsSnapshot = { ...this.nextIds };
let fixEvidenceStarted = false;
let mutationPolicyGraphStarted = false;
try {
this.graphSaveActive = true;
this.graphCommitActions = [];
this.graphRollbackActions = [];
this.userContext.insertResource("fixTime", new Date());
this.userContext.beginFixEvidence();
fixEvidenceStarted = true;
this.userContext.beginMutationPolicyGraph();
mutationPolicyGraphStarted = true;
const result = await work(graph);
this.userContext.ensureMutationPolicyGraphComplete();
this.persist();
for (const action of this.graphCommitActions) action();
return result;
} catch (error) {
this.data = dataSnapshot;
this.nextIds = nextIdsSnapshot;
for (const action of [...this.graphRollbackActions].reverse()) action();
throw error;
} finally {
this.graphSaveActive = false;
this.graphCommitActions = [];
this.graphRollbackActions = [];
if (mutationPolicyGraphStarted) this.userContext.endMutationPolicyGraph();
this.userContext.removeResource("fixTime");
if (fixEvidenceStarted) this.userContext.finishFixEvidence();
release();
}
}
afterGraphCommit(work: () => void): void {
if (!this.graphSaveActive) throw new Error("No graph save is active");
this.graphCommitActions.push(work);
}
afterGraphRollback(work: () => void): void {
if (!this.graphSaveActive) throw new Error("No graph save is active");
this.graphRollbackActions.push(work);
}
preflightMutation(mutation: any): any {
mutation = this.checkAndFixMutation(mutation);
this.userContext.recordMutationPolicyPreflight(mutation);
return mutation;
}
private checkAndFixMutation(mutation: any): any {
if (mutation instanceof MutationRequest) mutation = mutation.mutation;
if (!String(mutation?.comment || "").trim()) throw new Error("Security audit failure: audit reason is required before mutation");
mutation = { ...mutation, payload: { ...(mutation?.payload || {}) } };
const checker = this.checkers[String(mutation.entity)];
if (!checker) return mutation;
const results: any[] = [];
const ownsFixTime = this.userContext.getResource("fixTime") === undefined;
if (ownsFixTime) this.userContext.insertResource("fixTime", new Date()).beginFixEvidence();
try {
checker.checkAndFix(this.userContext, mutation, results);
if (mutation.ledgerKey && mutation.ledgerRoot) for (const [field, value] of Object.entries(mutation.payload)) mutation.ledgerRoot.set(mutation.ledgerKey, field, value);
this.userContext.translateCheckResults(results);
if (results.length) throw new CheckException(results);
return mutation;
} finally { if (ownsFixTime) this.userContext.removeResource("fixTime").finishFixEvidence(); }
}
async executeMutation(mutation: any): Promise<any> {
mutation = this.checkAndFixMutation(mutation);
const mutationGovernance = this.userContext.enterMutationPolicy(mutation);
this.mutationGovernanceEvents.push(mutationGovernance);
const table = this.data[mutation.entity] ||= {};
if (mutation.action === "Create") {
const id = mutation.id ?? String(this.nextIds[mutation.entity] || 1);
this.nextIds[mutation.entity] = Number(id) + 1;
const record = { ...mutation.payload, id: String(id), version: Number(mutation.version || 0) + 1 };
table[String(id)] = record;
if (!this.graphSaveActive) this.persist();
return { success: true, id: String(id), version: record.version, persistedRecord: { ...record } };
}
if (mutation.action === "Update") {
const id = String(mutation.id);
if (!table[id]) throw new Error(`${mutation.entity}(${id}) does not exist`);
const expectedVersion = Number(mutation.version);
const currentVersion = Number(table[id].version || 0);
if (!Number.isFinite(expectedVersion) || expectedVersion !== currentVersion) {
throw new Error(
`Optimistic lock conflict for ${mutation.entity}(${id}): expected ${expectedVersion}, current ${currentVersion}`);
}
table[id] = { ...table[id], ...mutation.payload, id, version: Number(table[id].version || 0) + 1 };
if (!this.graphSaveActive) this.persist();
return { success: true, id, version: table[id].version, persistedRecord: { ...table[id] } };
}
if (mutation.action === "Delete") {
const id = String(mutation.id);
if (!table[id]) throw new Error(`${mutation.entity}(${id}) does not exist`);
const expectedVersion = Number(mutation.version);
const currentVersion = Number(table[id].version || 0);
if (!Number.isFinite(expectedVersion) || expectedVersion !== currentVersion) {
throw new Error(
`Optimistic lock conflict for ${mutation.entity}(${id}): expected ${expectedVersion}, current ${currentVersion}`);
}
table[id] = { ...table[id], id, version: -(currentVersion + 1) };
if (!this.graphSaveActive) this.persist();
return { success: true, id, version: table[id].version, deleted: true, persistedRecord: { ...table[id] } };
}
throw new Error(`Unsupported mutation action: ${mutation.action}`);
}
async query(context: any, req: any): Promise<any> {
return { rows: [] };
}
async executeQuery(query: any): Promise<any> {
if (!(query instanceof RuntimeSelectQuery)) {
throw new Error("TeaQL list execution requires the formal runtime SelectQuery");
}
query.prepareForList();
let rows = Object.values(this.data[query.entity] || {}).map((row: any) => ({ ...row }));
const matches = (row: any, expression: any): boolean => {
if (expression?.$and) return expression.$and.every((item: any) => matches(row, item));
return Object.entries(expression || {}).every(([field, predicate]: [string, any]) => {
if (predicate?.$eq !== undefined) return row[field] === (predicate.$eq?.id ?? predicate.$eq);
if (predicate?.$ne !== undefined) return row[field] !== (predicate.$ne?.id ?? predicate.$ne);
if (predicate?.$contains !== undefined) return String(row[field] ?? "").includes(String(predicate.$contains));
if (predicate?.$notContains !== undefined) return !String(row[field] ?? "").includes(String(predicate.$notContains));
if (predicate?.$startsWith !== undefined) return String(row[field] ?? "").startsWith(String(predicate.$startsWith));
if (predicate?.$notStartsWith !== undefined) return !String(row[field] ?? "").startsWith(String(predicate.$notStartsWith));
if (predicate?.$endsWith !== undefined) return String(row[field] ?? "").endsWith(String(predicate.$endsWith));
if (predicate?.$notEndsWith !== undefined) return !String(row[field] ?? "").endsWith(String(predicate.$notEndsWith));
if (predicate?.$soundLike !== undefined) return soundex(row[field]) === soundex(predicate.$soundLike);
if (predicate?.$in !== undefined) return predicate.$in.some((value: any) => row[field] === (value?.id ?? value));
if (predicate?.$notIn !== undefined) return !predicate.$notIn.some((value: any) => row[field] === (value?.id ?? value));
if (predicate?.$between !== undefined) return row[field] >= predicate.$between[0] && row[field] <= predicate.$between[1];
if (predicate?.$isNull === true) return row[field] === null || row[field] === undefined;
if (predicate?.$isNull === false) return row[field] !== null && row[field] !== undefined;
if (predicate?.$gt !== undefined) return row[field] > predicate.$gt;
if (predicate?.$gte !== undefined) return row[field] >= predicate.$gte;
if (predicate?.$lt !== undefined) return row[field] < predicate.$lt;
if (predicate?.$lte !== undefined) return row[field] <= predicate.$lte;
return true;
});
};
if (query.filterCondition) rows = rows.filter(row => matches(row, query.filterCondition));
if (query.aggregateItems?.length) {
return [Object.fromEntries(query.aggregateItems.map((aggregate: any) => {
if (String(aggregate.function).toLowerCase() !== "count") {
throw new Error(`Unsupported local aggregate: ${aggregate.function}`);
}
return [aggregate.alias, rows.length];
}))];
}
for (const order of [...query.orderItems].reverse()) {
rows.sort((a, b) => (a[order.field] === b[order.field] ? 0 : a[order.field] > b[order.field] ? 1 : -1) * (String(order.direction).toLowerCase() === "desc" ? -1 : 1));
}
const start = query.offsetValue || 0;
rows = rows.slice(start, start + query.limitValue);
for (const relation of (query.relations || []) as any[]) {
const localValues = new Set(rows.map((row: any) => row[relation.localKey || "id"]));
const relatedRows = (await this.executeQuery(relation.query)).filter(
(row: any) => localValues.has(row[relation.foreignKey || "id"]));
for (const row of rows) {
const matches = relatedRows.filter(
(related: any) => related[relation.foreignKey || "id"] === row[relation.localKey || "id"]);
row[relation.name] = relation.many === false ? matches[0] : matches;
}
}
if (query.selectItems.length > 0) {
const selections = new Set(query.selectItems);
for (const relation of (query.relations || []) as any[]) selections.add(relation.name);
rows = rows.map((row: any) => Object.fromEntries(
Object.entries(row).filter(([field]) => selections.has(field))));
}
return rows;
}
async executeCount(query: any, countAlias?: string, operation?: QueryOperation): Promise<number> {
if (typeof query?.forExactCount !== "function") {
throw new Error("TeaQL exact count requires the formal runtime SelectQuery");
}
const alias = countAlias ?? "__teaql_total";
const request = new QueryRequest(query).deriveOperation(query.forExactCount(alias), operation ?? {
kind: "named-count", ...(countAlias === undefined ? {} : { alias: countAlias }) });
const rows = await this.executeQuery(request.query);
const value = rows[0]?.[alias];
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new Error(`TeaQL provider did not return exact count alias ${alias}`);
}
return value;
}
async *executeForStream(query: any, chunkSize = 1000): AsyncIterable<any[]> {
if (!Number.isInteger(chunkSize) || chunkSize <= 0) throw new Error("stream chunk size must be a positive integer");
const rows = await this.executeQuery(query);
for (let offset = 0; offset < rows.length; offset += chunkSize) {
yield rows.slice(offset, offset + chunkSize);
}
}
}
declare const require: any;
export function eq(a: any, b: any) { return {type: 'eq', field: a, value: b}; }
export function contain(a: any, b: any) { return {type: 'contain', field: a, value: b}; } |