diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.description.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.description.md new file mode 100644 index 0000000000000000000000000000000000000000..4400bb2620277f5ba33ec38a8c23070beb3fcf8e --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.description.md @@ -0,0 +1,71 @@ + + +# TypeScript Query Field Assist — `customer_order.description` + +KSML entity: `Customer Order` +KSML field: `description` +Type: `string` + +| Capability | Generated API | +| --- | --- | +| Select | `selectDescription()` | +| Equality/set | `withDescriptionIs(value)`, `withDescriptionIsNot(value)`, `withDescriptionIn(...values)`, `withDescriptionNotIn(...values)` | +| Comparison | `withDescriptionGreaterThan(value)`, `withDescriptionGreaterThanOrEqualTo(value)`, `withDescriptionLessThan(value)`, `withDescriptionLessThanOrEqualTo(value)`, `withDescriptionBetween(lower, upper)` | +| Null state | `withDescriptionIsKnown()`, `withDescriptionIsUnknown()` | +| String | `withDescriptionContaining(value)`, `withDescriptionNotContaining(value)`, `withDescriptionStartingWith(value)`, `withDescriptionEndingWith(value)`, `withDescriptionSoundingLike(value)` | +| Order | `orderByDescriptionAscending()`, `orderByDescriptionDescending()` | +| Group | `groupByDescription()`, `groupByDescriptionAs(alias)` | +Execution requires `comment(...)`, `purpose(...)`, and exactly one `context`. + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.id.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.id.md new file mode 100644 index 0000000000000000000000000000000000000000..d0eee62b11dee853b179e96b9ad580138bcc5dc1 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.id.md @@ -0,0 +1,70 @@ + + +# TypeScript Query Field Assist — `customer_order.id` + +KSML entity: `Customer Order` +KSML field: `id` +Type: `id` + +| Capability | Generated API | +| --- | --- | +| Select | `selectId()` | +| Equality/set | `withIdIs(value)`, `withIdIsNot(value)`, `withIdIn(...values)`, `withIdNotIn(...values)` | +| Comparison | `withIdGreaterThan(value)`, `withIdGreaterThanOrEqualTo(value)`, `withIdLessThan(value)`, `withIdLessThanOrEqualTo(value)`, `withIdBetween(lower, upper)` | +| Null state | `withIdIsKnown()`, `withIdIsUnknown()` | +| Order | `orderByIdAscending()`, `orderByIdDescending()` | +| Group | `groupById()`, `groupByIdAs(alias)` | +Execution requires `comment(...)`, `purpose(...)`, and exactly one `context`. + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.md new file mode 100644 index 0000000000000000000000000000000000000000..3ce4e2f9e44e827dead0ad0927b5a8b9d54640fe --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.md @@ -0,0 +1,104 @@ + + +# TypeScript Assist — Query `Customer Order` + +Use the exact generated `Q.customerOrdersWithMinimalFields()` +entry point. Trusted runtime and policy state comes only from `UserContext`. + +```typescript +import { Q } from './Q'; +import { CustomerOrder } from './models/CustomerOrder'; +import { SmartList, UserContext } from '../teaql-ts'; + +export async function queryCustomerOrderById( + context: UserContext, + entityId: string, +): Promise> { + return Q.customerOrdersWithMinimalFields() + .withIdIs(entityId) + .orderByIdAscending() + .limit(20) + .comment('what: load the requested Customer Order row') + .purpose('why: serve the authorized Customer Order view') + .executeForList(context); +} +``` + +Compile and execute this source unchanged. Use only generated projection, +predicate, ordering, and relation-selection APIs. Missing or blank intent, +unknown fields, unbounded reads, and trusted-context inputs must fail. Use generated +relation loading rather than handwritten child-query loops; never guess a plural. + + +## Field-specific Query Assist + +Use the canonical KSML field name from this list. Do not substitute a language member name, JSON name, or database column. + +| KSML field | Type | Field help | +| --- | --- | --- | +| `id` | `id` | `typescript-assist-query/customer_order.id` | +| `platform` | `relation` | `typescript-assist-query/customer_order.platform` | +| `order_number` | `string` | `typescript-assist-query/customer_order.order_number` | +| `description` | `string` | `typescript-assist-query/customer_order.description` | +| `version` | `version` | `typescript-assist-query/customer_order.version` | + +Generated reverse relations are derived from referencing KSML fields. Use their exact generated location for relation selection and access: + +| Generated reverse relation | Child entity | Field help | +| --- | --- | --- | +| `order_item_list` | `order_item` | `typescript-assist-query/customer_order.order_item_list` | +| `payment_list` | `payment` | `typescript-assist-query/customer_order.payment_list` | +| `shipment_list` | `shipment` | `typescript-assist-query/customer_order.shipment_list` | + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.order_item_list.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.order_item_list.md new file mode 100644 index 0000000000000000000000000000000000000000..a959200c82f1dfcf9a3e621a788b7070ab60c27c --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.order_item_list.md @@ -0,0 +1,71 @@ + + +# Query Reverse-Relation Assist — `customer_order.order_item_list` + +KSML entity: `customer_order` +Derived reverse relation: `order_item_list` +Child entity: `order_item` + +| Capability | Generated API | +| --- | --- | +| Select | `.selectOrderItemListWith(Q.orderItemsWithMinimalFields())` | +| Related count | `.countOrderItemsAs(alias)` | +| Filtered related count | `.countOrderItemsWith(alias, childRequest)` | +| Read related count | `(entity as unknown as Record)[alias]`; require a present numeric value | + +Related counts are query projections, not modeled fields or mutation setters. Use a dedicated child request for the aggregate; do not reuse it as a row selection. An alias has no generated E accessor. Use E for modeled fields and loaded relation traversal; then narrow the returned alias from unknown. Missing is not zero. List and stream return entities carrying the alias; executeForRows returns records. Choose aliases that do not collide with model fields, relations or entity methods. + +Reverse relations are model-derived and never perform an implicit database query. Select the relation explicitly before expression access. Apply query methods before the executable purpose stage, then execute with exactly one trusted context. + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.payment_list.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.payment_list.md new file mode 100644 index 0000000000000000000000000000000000000000..61b2a191a30866a827beef44e4a0e7b627b6a2f3 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.payment_list.md @@ -0,0 +1,71 @@ + + +# Query Reverse-Relation Assist — `customer_order.payment_list` + +KSML entity: `customer_order` +Derived reverse relation: `payment_list` +Child entity: `payment` + +| Capability | Generated API | +| --- | --- | +| Select | `.selectPaymentListWith(Q.paymentsWithMinimalFields())` | +| Related count | `.countPaymentsAs(alias)` | +| Filtered related count | `.countPaymentsWith(alias, childRequest)` | +| Read related count | `(entity as unknown as Record)[alias]`; require a present numeric value | + +Related counts are query projections, not modeled fields or mutation setters. Use a dedicated child request for the aggregate; do not reuse it as a row selection. An alias has no generated E accessor. Use E for modeled fields and loaded relation traversal; then narrow the returned alias from unknown. Missing is not zero. List and stream return entities carrying the alias; executeForRows returns records. Choose aliases that do not collide with model fields, relations or entity methods. + +Reverse relations are model-derived and never perform an implicit database query. Select the relation explicitly before expression access. Apply query methods before the executable purpose stage, then execute with exactly one trusted context. + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.platform.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.platform.md new file mode 100644 index 0000000000000000000000000000000000000000..aa3a43278e69ed1415f2b538efe35ac76e626b09 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.platform.md @@ -0,0 +1,69 @@ + + +# TypeScript Query Field Assist — `customer_order.platform` + +KSML entity: `Customer Order` +KSML field: `Platform` +Type: `Platform` + +| Capability | Generated API | +| --- | --- | +| Select | `selectPlatformWith(request)` | +| Filter identity | `filterByPlatform(id)`, `filterByPlatformIn(...ids)` | +| Filter nested request | `withPlatformMatching(request)`, `withoutPlatformMatching(request)` | +| Group | `groupByPlatform()`, `groupByPlatformAs(alias)` | +| Facet | `facetByPlatformAs(name, request, includeAllFacets)` | +Execution requires `comment(...)`, `purpose(...)`, and exactly one `context`. + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.shipment_list.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.shipment_list.md new file mode 100644 index 0000000000000000000000000000000000000000..d84ed31c2e09e31e8244718308cdf6f3295f11ab --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/customer_order.shipment_list.md @@ -0,0 +1,71 @@ + + +# Query Reverse-Relation Assist — `customer_order.shipment_list` + +KSML entity: `customer_order` +Derived reverse relation: `shipment_list` +Child entity: `shipment` + +| Capability | Generated API | +| --- | --- | +| Select | `.selectShipmentListWith(Q.shipmentsWithMinimalFields())` | +| Related count | `.countShipmentsAs(alias)` | +| Filtered related count | `.countShipmentsWith(alias, childRequest)` | +| Read related count | `(entity as unknown as Record)[alias]`; require a present numeric value | + +Related counts are query projections, not modeled fields or mutation setters. Use a dedicated child request for the aggregate; do not reuse it as a row selection. An alias has no generated E accessor. Use E for modeled fields and loaded relation traversal; then narrow the returned alias from unknown. Missing is not zero. List and stream return entities carrying the alias; executeForRows returns records. Choose aliases that do not collide with model fields, relations or entity methods. + +Reverse relations are model-derived and never perform an implicit database query. Select the relation explicitly before expression access. Apply query methods before the executable purpose stage, then execute with exactly one trusted context. + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/order_item.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/order_item.md new file mode 100644 index 0000000000000000000000000000000000000000..30d448eb702f4fa6a7852dd17cd4a010e554ffd8 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/order_item.md @@ -0,0 +1,95 @@ + + +# TypeScript Assist — Query `Order Item` + +Use the exact generated `Q.orderItemsWithMinimalFields()` +entry point. Trusted runtime and policy state comes only from `UserContext`. + +```typescript +import { Q } from './Q'; +import { OrderItem } from './models/OrderItem'; +import { SmartList, UserContext } from '../teaql-ts'; + +export async function queryOrderItemById( + context: UserContext, + entityId: string, +): Promise> { + return Q.orderItemsWithMinimalFields() + .withIdIs(entityId) + .orderByIdAscending() + .limit(20) + .comment('what: load the requested Order Item row') + .purpose('why: serve the authorized Order Item view') + .executeForList(context); +} +``` + +Compile and execute this source unchanged. Use only generated projection, +predicate, ordering, and relation-selection APIs. Missing or blank intent, +unknown fields, unbounded reads, and trusted-context inputs must fail. Use generated +relation loading rather than handwritten child-query loops; never guess a plural. + + +## Field-specific Query Assist + +Use the canonical KSML field name from this list. Do not substitute a language member name, JSON name, or database column. + +| KSML field | Type | Field help | +| --- | --- | --- | +| `id` | `id` | `typescript-assist-query/order_item.id` | +| `customer_order` | `relation` | `typescript-assist-query/order_item.customer_order` | +| `name` | `string` | `typescript-assist-query/order_item.name` | +| `version` | `version` | `typescript-assist-query/order_item.version` | + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/order_item.name.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/order_item.name.md new file mode 100644 index 0000000000000000000000000000000000000000..844d7df459fbf43da313ffceb5460e426f10c47d --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/order_item.name.md @@ -0,0 +1,71 @@ + + +# TypeScript Query Field Assist — `order_item.name` + +KSML entity: `Order Item` +KSML field: `name` +Type: `string` + +| Capability | Generated API | +| --- | --- | +| Select | `selectName()` | +| Equality/set | `withNameIs(value)`, `withNameIsNot(value)`, `withNameIn(...values)`, `withNameNotIn(...values)` | +| Comparison | `withNameGreaterThan(value)`, `withNameGreaterThanOrEqualTo(value)`, `withNameLessThan(value)`, `withNameLessThanOrEqualTo(value)`, `withNameBetween(lower, upper)` | +| Null state | `withNameIsKnown()`, `withNameIsUnknown()` | +| String | `withNameContaining(value)`, `withNameNotContaining(value)`, `withNameStartingWith(value)`, `withNameEndingWith(value)`, `withNameSoundingLike(value)` | +| Order | `orderByNameAscending()`, `orderByNameDescending()` | +| Group | `groupByName()`, `groupByNameAs(alias)` | +Execution requires `comment(...)`, `purpose(...)`, and exactly one `context`. + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment.customer_order.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment.customer_order.md new file mode 100644 index 0000000000000000000000000000000000000000..67637f76f7ae0a1ae59b3252a68faa6a7c2231f0 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment.customer_order.md @@ -0,0 +1,69 @@ + + +# TypeScript Query Field Assist — `payment.customer_order` + +KSML entity: `Payment` +KSML field: `Customer Order` +Type: `Customer Order` + +| Capability | Generated API | +| --- | --- | +| Select | `selectCustomerOrderWith(request)` | +| Filter identity | `filterByCustomerOrder(id)`, `filterByCustomerOrderIn(...ids)` | +| Filter nested request | `withCustomerOrderMatching(request)`, `withoutCustomerOrderMatching(request)` | +| Group | `groupByCustomerOrder()`, `groupByCustomerOrderAs(alias)` | +| Facet | `facetByCustomerOrderAs(name, request, includeAllFacets)` | +Execution requires `comment(...)`, `purpose(...)`, and exactly one `context`. + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment.md new file mode 100644 index 0000000000000000000000000000000000000000..3957754426871005d2cba06da7e64457f1a2e4b6 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment.md @@ -0,0 +1,101 @@ + + +# TypeScript Assist — Query `Payment` + +Use the exact generated `Q.paymentsWithMinimalFields()` +entry point. Trusted runtime and policy state comes only from `UserContext`. + +```typescript +import { Q } from './Q'; +import { Payment } from './models/Payment'; +import { SmartList, UserContext } from '../teaql-ts'; + +export async function queryPaymentById( + context: UserContext, + entityId: string, +): Promise> { + return Q.paymentsWithMinimalFields() + .withIdIs(entityId) + .orderByIdAscending() + .limit(20) + .comment('what: load the requested Payment row') + .purpose('why: serve the authorized Payment view') + .executeForList(context); +} +``` + +Compile and execute this source unchanged. Use only generated projection, +predicate, ordering, and relation-selection APIs. Missing or blank intent, +unknown fields, unbounded reads, and trusted-context inputs must fail. Use generated +relation loading rather than handwritten child-query loops; never guess a plural. + + +## Field-specific Query Assist + +Use the canonical KSML field name from this list. Do not substitute a language member name, JSON name, or database column. + +| KSML field | Type | Field help | +| --- | --- | --- | +| `id` | `id` | `typescript-assist-query/payment.id` | +| `customer_order` | `relation` | `typescript-assist-query/payment.customer_order` | +| `reference_code` | `string` | `typescript-assist-query/payment.reference_code` | +| `version` | `version` | `typescript-assist-query/payment.version` | + +Generated reverse relations are derived from referencing KSML fields. Use their exact generated location for relation selection and access: + +| Generated reverse relation | Child entity | Field help | +| --- | --- | --- | +| `payment_attempt_list` | `payment_attempt` | `typescript-assist-query/payment.payment_attempt_list` | + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment.payment_attempt_list.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment.payment_attempt_list.md new file mode 100644 index 0000000000000000000000000000000000000000..024d06653f6b4d0624387c8a7bdf9c336475727b --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment.payment_attempt_list.md @@ -0,0 +1,71 @@ + + +# Query Reverse-Relation Assist — `payment.payment_attempt_list` + +KSML entity: `payment` +Derived reverse relation: `payment_attempt_list` +Child entity: `payment_attempt` + +| Capability | Generated API | +| --- | --- | +| Select | `.selectPaymentAttemptListWith(Q.paymentAttemptsWithMinimalFields())` | +| Related count | `.countPaymentAttemptsAs(alias)` | +| Filtered related count | `.countPaymentAttemptsWith(alias, childRequest)` | +| Read related count | `(entity as unknown as Record)[alias]`; require a present numeric value | + +Related counts are query projections, not modeled fields or mutation setters. Use a dedicated child request for the aggregate; do not reuse it as a row selection. An alias has no generated E accessor. Use E for modeled fields and loaded relation traversal; then narrow the returned alias from unknown. Missing is not zero. List and stream return entities carrying the alias; executeForRows returns records. Choose aliases that do not collide with model fields, relations or entity methods. + +Reverse relations are model-derived and never perform an implicit database query. Select the relation explicitly before expression access. Apply query methods before the executable purpose stage, then execute with exactly one trusted context. + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment.reference_code.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment.reference_code.md new file mode 100644 index 0000000000000000000000000000000000000000..bf3a6b84b9936cba928a1ec794194c51b0491014 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment.reference_code.md @@ -0,0 +1,71 @@ + + +# TypeScript Query Field Assist — `payment.reference_code` + +KSML entity: `Payment` +KSML field: `reference_code` +Type: `string` + +| Capability | Generated API | +| --- | --- | +| Select | `selectReferenceCode()` | +| Equality/set | `withReferenceCodeIs(value)`, `withReferenceCodeIsNot(value)`, `withReferenceCodeIn(...values)`, `withReferenceCodeNotIn(...values)` | +| Comparison | `withReferenceCodeGreaterThan(value)`, `withReferenceCodeGreaterThanOrEqualTo(value)`, `withReferenceCodeLessThan(value)`, `withReferenceCodeLessThanOrEqualTo(value)`, `withReferenceCodeBetween(lower, upper)` | +| Null state | `withReferenceCodeIsKnown()`, `withReferenceCodeIsUnknown()` | +| String | `withReferenceCodeContaining(value)`, `withReferenceCodeNotContaining(value)`, `withReferenceCodeStartingWith(value)`, `withReferenceCodeEndingWith(value)`, `withReferenceCodeSoundingLike(value)` | +| Order | `orderByReferenceCodeAscending()`, `orderByReferenceCodeDescending()` | +| Group | `groupByReferenceCode()`, `groupByReferenceCodeAs(alias)` | +Execution requires `comment(...)`, `purpose(...)`, and exactly one `context`. + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment_attempt.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment_attempt.md new file mode 100644 index 0000000000000000000000000000000000000000..80107f9eda840002f35895c378a2d54585af3dce --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment_attempt.md @@ -0,0 +1,95 @@ + + +# TypeScript Assist — Query `Payment Attempt` + +Use the exact generated `Q.paymentAttemptsWithMinimalFields()` +entry point. Trusted runtime and policy state comes only from `UserContext`. + +```typescript +import { Q } from './Q'; +import { PaymentAttempt } from './models/PaymentAttempt'; +import { SmartList, UserContext } from '../teaql-ts'; + +export async function queryPaymentAttemptById( + context: UserContext, + entityId: string, +): Promise> { + return Q.paymentAttemptsWithMinimalFields() + .withIdIs(entityId) + .orderByIdAscending() + .limit(20) + .comment('what: load the requested Payment Attempt row') + .purpose('why: serve the authorized Payment Attempt view') + .executeForList(context); +} +``` + +Compile and execute this source unchanged. Use only generated projection, +predicate, ordering, and relation-selection APIs. Missing or blank intent, +unknown fields, unbounded reads, and trusted-context inputs must fail. Use generated +relation loading rather than handwritten child-query loops; never guess a plural. + + +## Field-specific Query Assist + +Use the canonical KSML field name from this list. Do not substitute a language member name, JSON name, or database column. + +| KSML field | Type | Field help | +| --- | --- | --- | +| `id` | `id` | `typescript-assist-query/payment_attempt.id` | +| `payment` | `relation` | `typescript-assist-query/payment_attempt.payment` | +| `reference_code` | `string` | `typescript-assist-query/payment_attempt.reference_code` | +| `version` | `version` | `typescript-assist-query/payment_attempt.version` | + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment_attempt.payment.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment_attempt.payment.md new file mode 100644 index 0000000000000000000000000000000000000000..3618163bb16b7534ed01138bcdc42f9ceb54ed00 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/payment_attempt.payment.md @@ -0,0 +1,69 @@ + + +# TypeScript Query Field Assist — `payment_attempt.payment` + +KSML entity: `Payment Attempt` +KSML field: `Payment` +Type: `Payment` + +| Capability | Generated API | +| --- | --- | +| Select | `selectPaymentWith(request)` | +| Filter identity | `filterByPayment(id)`, `filterByPaymentIn(...ids)` | +| Filter nested request | `withPaymentMatching(request)`, `withoutPaymentMatching(request)` | +| Group | `groupByPayment()`, `groupByPaymentAs(alias)` | +| Facet | `facetByPaymentAs(name, request, includeAllFacets)` | +Execution requires `comment(...)`, `purpose(...)`, and exactly one `context`. + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/platform.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/platform.md new file mode 100644 index 0000000000000000000000000000000000000000..0ac80c4fbde8752a1c2fba40114aa28a8a247588 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-query/platform.md @@ -0,0 +1,100 @@ + + +# TypeScript Assist — Query `Platform` + +Use the exact generated `Q.platformsWithMinimalFields()` +entry point. Trusted runtime and policy state comes only from `UserContext`. + +```typescript +import { Q } from './Q'; +import { Platform } from './models/Platform'; +import { SmartList, UserContext } from '../teaql-ts'; + +export async function queryPlatformById( + context: UserContext, + entityId: string, +): Promise> { + return Q.platformsWithMinimalFields() + .withIdIs(entityId) + .orderByIdAscending() + .limit(20) + .comment('what: load the requested Platform row') + .purpose('why: serve the authorized Platform view') + .executeForList(context); +} +``` + +Compile and execute this source unchanged. Use only generated projection, +predicate, ordering, and relation-selection APIs. Missing or blank intent, +unknown fields, unbounded reads, and trusted-context inputs must fail. Use generated +relation loading rather than handwritten child-query loops; never guess a plural. + + +## Field-specific Query Assist + +Use the canonical KSML field name from this list. Do not substitute a language member name, JSON name, or database column. + +| KSML field | Type | Field help | +| --- | --- | --- | +| `id` | `id` | `typescript-assist-query/platform.id` | +| `name` | `string` | `typescript-assist-query/platform.name` | +| `version` | `version` | `typescript-assist-query/platform.version` | + +Generated reverse relations are derived from referencing KSML fields. Use their exact generated location for relation selection and access: + +| Generated reverse relation | Child entity | Field help | +| --- | --- | --- | +| `customer_order_list` | `customer_order` | `typescript-assist-query/platform.customer_order_list` | + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `query`. + +- Use generated projection, relation, predicate, sort, and aggregate APIs; never + concatenate application SQL or silently ignore an unsupported filter. +- Rows, record count, facets, and aggregates must share the same active filter. +- Add negative tests for missing purpose/comment and forbidden dynamic fields. + +### Optional per-parent Top-N optimization + +- Configure `topNProbeParentThreshold(threshold)` on the nested child request before its intent/execution + stage. Use the field-specific relation Assist for the exact generated selector. +- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker; + bound the parent query separately. A single-parent detail query is not batch Top-N. +- Server providers default to a window query (`threshold = 0`). A positive threshold + permits bounded probes only when the already-loaded parent count is at or below it; + above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default. +- Measure before opting in. Do not infer a threshold from table size or execute COUNT + or statistics queries to choose a plan. Keep an index on foreign key + sort + ID. +- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry. + Runtime-managed probes are not application-owned N+1 loops; neither strategy changes + authorization, loaded-state semantics or the required comment/purpose. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-update/customer_order.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-update/customer_order.md new file mode 100644 index 0000000000000000000000000000000000000000..b6678dcca0805244dd049f4b8fea0e139aca0113 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-update/customer_order.md @@ -0,0 +1,93 @@ + + +# TypeScript Assist — Update `Customer Order` + +Load the current row and its original optimistic version. Never reconstruct an +entity from an ID/version supplied by a client. Trusted runtime and policy state +comes only from `UserContext`. + +An entity update is not a partial DTO patch. Load every scalar field before +modification so checker rules can validate the complete business state. Never +use `WithMinimalFields()` or a reduced projection for an entity that will be saved. + +```typescript +import { UserContext } from '../teaql-ts'; +import { Q } from './Q'; +import { CustomerOrder } from './models/CustomerOrder'; + +export interface UpdateCustomerOrderInput { + orderNumber: string; + description: string; + +} + +export async function updateCustomerOrder( + id: string, + input: UpdateCustomerOrderInput, + context: UserContext, + throwIfMissing = false, +): Promise { + const entity = await Q.customerOrders() + .withIdIs(id) + + .comment('what: load current Customer Order for update') + .purpose('why: preserve original version for optimistic locking') + .executeForOne(context); + if (!entity) { + if (throwIfMissing) throw new Error('Customer Order not found: ' + id); + return undefined; + } + + entity.updateOrderNumber(input.orderNumber); + entity.updateDescription(input.description); + + await entity + .auditAs('Update Customer Order for the requested business operation') + .save(context); + return entity; +} +``` + +Compile the source unchanged. Prove persistence and query-back, a conflict from +an independently loaded stale copy, undefined and throwing not-found behavior, +missing/blank audit rejection, and compilation failure for unknown or trusted +fields. Constant and relation updates require an explicitly selected generated +method; do not invent a generic setter. + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `update`. + +- Load the tenant-scoped current entity first so its original version participates + in optimistic locking; do not reconstruct versioned state from untrusted JSON. +- Allow-list writable fields, attach the generated audit-reason API, and save with + the same UserContext. Add a stale-version rejection test. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-update/order_item.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-update/order_item.md new file mode 100644 index 0000000000000000000000000000000000000000..749e7ba418857df538ed7d0e8faa79c6008c0c2c --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-update/order_item.md @@ -0,0 +1,91 @@ + + +# TypeScript Assist — Update `Order Item` + +Load the current row and its original optimistic version. Never reconstruct an +entity from an ID/version supplied by a client. Trusted runtime and policy state +comes only from `UserContext`. + +An entity update is not a partial DTO patch. Load every scalar field before +modification so checker rules can validate the complete business state. Never +use `WithMinimalFields()` or a reduced projection for an entity that will be saved. + +```typescript +import { UserContext } from '../teaql-ts'; +import { Q } from './Q'; +import { OrderItem } from './models/OrderItem'; + +export interface UpdateOrderItemInput { + name: string; + +} + +export async function updateOrderItem( + id: string, + input: UpdateOrderItemInput, + context: UserContext, + throwIfMissing = false, +): Promise { + const entity = await Q.orderItems() + .withIdIs(id) + + .comment('what: load current Order Item for update') + .purpose('why: preserve original version for optimistic locking') + .executeForOne(context); + if (!entity) { + if (throwIfMissing) throw new Error('Order Item not found: ' + id); + return undefined; + } + + entity.updateName(input.name); + + await entity + .auditAs('Update Order Item for the requested business operation') + .save(context); + return entity; +} +``` + +Compile the source unchanged. Prove persistence and query-back, a conflict from +an independently loaded stale copy, undefined and throwing not-found behavior, +missing/blank audit rejection, and compilation failure for unknown or trusted +fields. Constant and relation updates require an explicitly selected generated +method; do not invent a generic setter. + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `update`. + +- Load the tenant-scoped current entity first so its original version participates + in optimistic locking; do not reconstruct versioned state from untrusted JSON. +- Allow-list writable fields, attach the generated audit-reason API, and save with + the same UserContext. Add a stale-version rejection test. diff --git a/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-update/payment.md b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-update/payment.md new file mode 100644 index 0000000000000000000000000000000000000000..3a0b117883e92fd82e518588d2310762945f5d70 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/assist/typescript-assist-update/payment.md @@ -0,0 +1,91 @@ + + +# TypeScript Assist — Update `Payment` + +Load the current row and its original optimistic version. Never reconstruct an +entity from an ID/version supplied by a client. Trusted runtime and policy state +comes only from `UserContext`. + +An entity update is not a partial DTO patch. Load every scalar field before +modification so checker rules can validate the complete business state. Never +use `WithMinimalFields()` or a reduced projection for an entity that will be saved. + +```typescript +import { UserContext } from '../teaql-ts'; +import { Q } from './Q'; +import { Payment } from './models/Payment'; + +export interface UpdatePaymentInput { + referenceCode: string; + +} + +export async function updatePayment( + id: string, + input: UpdatePaymentInput, + context: UserContext, + throwIfMissing = false, +): Promise { + const entity = await Q.payments() + .withIdIs(id) + + .comment('what: load current Payment for update') + .purpose('why: preserve original version for optimistic locking') + .executeForOne(context); + if (!entity) { + if (throwIfMissing) throw new Error('Payment not found: ' + id); + return undefined; + } + + entity.updateReferenceCode(input.referenceCode); + + await entity + .auditAs('Update Payment for the requested business operation') + .save(context); + return entity; +} +``` + +Compile the source unchanged. Prove persistence and query-back, a conflict from +an independently loaded stale copy, undefined and throwing not-found behavior, +missing/blank audit rejection, and compilation failure for unknown or trusted +fields. Constant and relation updates require an explicitly selected generated +method; do not invent a generic setter. + + +--- + +## TeaQL seven-language assist contract + +Apply the verified Rust semantic ceiling while using only the exact TYPESCRIPT generated and +runtime APIs. Discover APIs through the generated application AGENTS.md and progressive +model-aware Assist. Do not inspect generated domain-library source. + +- Do not create plurals by appending `s` or `es`; use the centralized generated plural. +- Human and non-human entities use different generated predicate vocabularies. Preserve + forms such as “who are active” and “whose email is”; never infer them from English. +- Configure filters, projection, paging, and other query options before `purpose(...)`. + Comment may appear anywhere in the chain. Purpose enters the executable stage; execution + requires both values, but comment does not have to immediately precede purpose. +- Every execute/list/stream and every save accepts exactly one context argument: + `UserContext`. Name that argument `context`, never `runtime`; data services and global + policy are injected when the context is built. Reserve `runtime` for process-level + runtime ownership, provider/pool setup, and module assembly. +- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit, + and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP. +- If the required operation is absent after current entity/action and required field + Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the + generated library as a fallback. +- Create each application-owned source file once. After its first compile attempt, + repair only the smallest block identified by the exact compiler or test diagnostic. + Preserve unrelated code; do not rewrite the complete file as an error-recovery loop. +- Before a repair that would replace more than 25% of an existing application file, + stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and + estimated scope. Initial creation and model-driven regeneration are not repairs. + +Capability: `update`. + +- Load the tenant-scoped current entity first so its original version participates + in optimistic locking; do not reconstruct versioned state from untrusted JSON. +- Allow-list writable fields, attach the generated audit-reason API, and save with + the same UserContext. Add a stale-version rejection test. diff --git a/source/teaql-ts/examples/trace-chain/evidence/evaluation.md b/source/teaql-ts/examples/trace-chain/evidence/evaluation.md new file mode 100644 index 0000000000000000000000000000000000000000..1342e1e17cecbe1d93d7f319cf0c40a7444060c9 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/evaluation.md @@ -0,0 +1,30 @@ + +# KSML Evaluation Report +## 📊 Summary +- **Errors**: 0 +- **Warnings**: 0 +- **Suggestions**: 0 +- **Solids**: 17 + +## ✅ Solids + +| Rule ID | Target | Source | Message | +|---------|--------|--------|---------| +| `KSML-UPLOAD-001` | `` | — | The upload contains 1 files. | +| `KSML-UPLOAD-005` | `` | `model.xml:2` | Resolved entrypoint: model.xml | +| `KSML-XML-002` | `` | `model.xml:2` | The XML document model.xml was parsed successfully. | +| `KSML-ROOT-003` | `` | `model.xml:2` | Root name 'trace-chain-service' is well-formed. | +| `KSML-ROOT-008` | `` | `model.xml:2` | External JSON business fields use the default camelCase profile. | +| `KSML-OBJECT-001` | `platform` | `model.xml:4` | Object 'platform' defines display name, module, and module key metadata. | +| `KSML-OBJECT-001` | `customer_order` | `model.xml:6` | Object 'customer_order' defines display name, module, and module key metadata. | +| `KSML-OBJECT-001` | `order_item` | `model.xml:9` | Object 'order_item' defines display name, module, and module key metadata. | +| `KSML-OBJECT-001` | `payment` | `model.xml:11` | Object 'payment' defines display name, module, and module key metadata. | +| `KSML-OBJECT-001` | `payment_attempt` | `model.xml:13` | Object 'payment_attempt' defines display name, module, and module key metadata. | +| `KSML-OBJECT-001` | `shipment` | `model.xml:15` | Object 'shipment' defines display name, module, and module key metadata. | +| `KSML-REFERENCE-003` | `customer_order.platform` | `model.xml:7` | Reference 'platform' in 'customer_order' successfully resolves to target object 'platform'. | +| `KSML-REFERENCE-003` | `order_item.customer_order` | `model.xml:10` | Reference 'customer_order' in 'order_item' successfully resolves to target object 'customer_order'. | +| `KSML-REFERENCE-003` | `payment.customer_order` | `model.xml:12` | Reference 'customer_order' in 'payment' successfully resolves to target object 'customer_order'. | +| `KSML-REFERENCE-003` | `payment_attempt.payment` | `model.xml:14` | Reference 'payment' in 'payment_attempt' successfully resolves to target object 'payment'. | +| `KSML-REFERENCE-003` | `shipment.customer_order` | `model.xml:16` | Reference 'customer_order' in 'shipment' successfully resolves to target object 'customer_order'. | +| `KSML-DOMAIN-ROOT-003` | `` | `model.xml:2` | Exactly one domain root candidate 'platform' was found. | + diff --git a/source/teaql-ts/examples/trace-chain/evidence/generated-library-manifest.sha256 b/source/teaql-ts/examples/trace-chain/evidence/generated-library-manifest.sha256 new file mode 100644 index 0000000000000000000000000000000000000000..edd07ba95ce383dd1b8ae048b26ab3ea73d18d16 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/generated-library-manifest.sha256 @@ -0,0 +1,22 @@ +665f49e26ecccfaaac239601a28d07064e6c9d87ec17e90e0e2e34fb7752884c package.json +d0e703595d462573c4ce298f1f96581dfc76e783c19baedfb1406241e6f2f26f src/generated/E.ts +d53eca569db8c6c7ed0bb7b76125bdeb0b2799c5ae678149e384bdfe23b03530 src/generated/models/CustomerOrder.ts +cae0253e18a9f9eff98d1c88c8f4483c839ec6ea70916b7e483b2977ff8ae785 src/generated/models/OrderItem.ts +a4dc14949d477f7e17a3118bfb7e8a1322a43766a918de439f685a2e3c330362 src/generated/models/Payment.ts +212167fdf2dcfd211b5eda9cc787150068ebb73c58e12faa4d86c6b4875d9924 src/generated/models/PaymentAttempt.ts +95913330ac20ceaa057ef965d9852151ea3df3054491d3baa91395ffaff8791c src/generated/models/Platform.ts +ff2f4ea6457c4f0c20de6fef52e5a6bdafa38b00ce30db028bdb8373a9490812 src/generated/models/Shipment.ts +ec633c6c5c488e3a2f0dd77a2c9cc5b40cece976f8439caf6782bb9f5cd49833 src/generated/Q.ts +11b8d4931d251c489e08995ae8a53766f0d567773246a38b32827091eeabaf64 src/generated/requests/CustomerOrderRequest.ts +4786b92f0fef3a62846e3e25c135dac4a4b7c8e227e24b6e6d3a6de111823582 src/generated/requests/OrderItemRequest.ts +4b406885fb61a6faa11757e775b041b301d9fbca4d22ce0cb08ade1ea7ec8775 src/generated/requests/PaymentAttemptRequest.ts +42c698c476bfae5c652eb604c2bde2209d2a085962f5835c6f6795500bb5c3fa src/generated/requests/PaymentRequest.ts +4336b0288835f02dc04ff0804a843c933f81d10f9d41f7d59c9783f9ae56dc79 src/generated/requests/PlatformRequest.ts +e38d62798230bd86c4ca5184165c6a9efdc44d506d090b1abb9bf02ea8c1e742 src/generated/requests/ShipmentRequest.ts +1d69981ee7ba6adcd404004e53c6d20fb4b480d11fafc18f6116930eae6a0783 src/generated-bootstrap.ts +b2793ced879531ebf0b7d1a87fbbe0357bad746f43b9d8fc900dce458dc07820 src/generated-search-models.ts +243bf811633ba73903dfa221890d7d6636f237753d595a84df96bd555639f7a0 src/runtime-module.ts +bf09100425eaa11c09ae3a412cea5706c2c60dcb0e4ed05effc5158b78a6e8a8 src/teaql-schemas.ts +cc705f9f7873450eae541311317db3402dc40b7cf068c440169bd7ba06dcb1ed src/teaql-ts.ts +5ff4a2f49563da43619a3bde0da4d3b73105e87824aef4851ba050b0509272d3 teaql-i18n.json +cf9a9767019ac7f4035a6d16952a5ae75b964b1c10c4584822b90799e6ce2037 tsconfig.json diff --git a/source/teaql-ts/examples/trace-chain/evidence/shared-reference-library.sha256 b/source/teaql-ts/examples/trace-chain/evidence/shared-reference-library.sha256 new file mode 100644 index 0000000000000000000000000000000000000000..edd07ba95ce383dd1b8ae048b26ab3ea73d18d16 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/evidence/shared-reference-library.sha256 @@ -0,0 +1,22 @@ +665f49e26ecccfaaac239601a28d07064e6c9d87ec17e90e0e2e34fb7752884c package.json +d0e703595d462573c4ce298f1f96581dfc76e783c19baedfb1406241e6f2f26f src/generated/E.ts +d53eca569db8c6c7ed0bb7b76125bdeb0b2799c5ae678149e384bdfe23b03530 src/generated/models/CustomerOrder.ts +cae0253e18a9f9eff98d1c88c8f4483c839ec6ea70916b7e483b2977ff8ae785 src/generated/models/OrderItem.ts +a4dc14949d477f7e17a3118bfb7e8a1322a43766a918de439f685a2e3c330362 src/generated/models/Payment.ts +212167fdf2dcfd211b5eda9cc787150068ebb73c58e12faa4d86c6b4875d9924 src/generated/models/PaymentAttempt.ts +95913330ac20ceaa057ef965d9852151ea3df3054491d3baa91395ffaff8791c src/generated/models/Platform.ts +ff2f4ea6457c4f0c20de6fef52e5a6bdafa38b00ce30db028bdb8373a9490812 src/generated/models/Shipment.ts +ec633c6c5c488e3a2f0dd77a2c9cc5b40cece976f8439caf6782bb9f5cd49833 src/generated/Q.ts +11b8d4931d251c489e08995ae8a53766f0d567773246a38b32827091eeabaf64 src/generated/requests/CustomerOrderRequest.ts +4786b92f0fef3a62846e3e25c135dac4a4b7c8e227e24b6e6d3a6de111823582 src/generated/requests/OrderItemRequest.ts +4b406885fb61a6faa11757e775b041b301d9fbca4d22ce0cb08ade1ea7ec8775 src/generated/requests/PaymentAttemptRequest.ts +42c698c476bfae5c652eb604c2bde2209d2a085962f5835c6f6795500bb5c3fa src/generated/requests/PaymentRequest.ts +4336b0288835f02dc04ff0804a843c933f81d10f9d41f7d59c9783f9ae56dc79 src/generated/requests/PlatformRequest.ts +e38d62798230bd86c4ca5184165c6a9efdc44d506d090b1abb9bf02ea8c1e742 src/generated/requests/ShipmentRequest.ts +1d69981ee7ba6adcd404004e53c6d20fb4b480d11fafc18f6116930eae6a0783 src/generated-bootstrap.ts +b2793ced879531ebf0b7d1a87fbbe0357bad746f43b9d8fc900dce458dc07820 src/generated-search-models.ts +243bf811633ba73903dfa221890d7d6636f237753d595a84df96bd555639f7a0 src/runtime-module.ts +bf09100425eaa11c09ae3a412cea5706c2c60dcb0e4ed05effc5158b78a6e8a8 src/teaql-schemas.ts +cc705f9f7873450eae541311317db3402dc40b7cf068c440169bd7ba06dcb1ed src/teaql-ts.ts +5ff4a2f49563da43619a3bde0da4d3b73105e87824aef4851ba050b0509272d3 teaql-i18n.json +cf9a9767019ac7f4035a6d16952a5ae75b964b1c10c4584822b90799e6ce2037 tsconfig.json diff --git a/source/teaql-ts/examples/trace-chain/lib/package.json b/source/teaql-ts/examples/trace-chain/lib/package.json new file mode 100644 index 0000000000000000000000000000000000000000..771b903e989c7e3a98ce410f33db25eeb6570ed0 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "trace-chain-service-lib", + "version": "1.0.0", + "description": "Generated typescript library", + "main": "src/generated/Q.ts", + "scripts": { + "build": "tsc --noEmit" + }, + "dependencies": { + "teaql-ts": "npm:@teaql/teaql@0.2.10"}, + "devDependencies": { + "typescript": "^5.0.0"} +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated-bootstrap.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated-bootstrap.ts new file mode 100644 index 0000000000000000000000000000000000000000..a2a4646143250d80e30eb74eb9a01fcff9ff1844 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated-bootstrap.ts @@ -0,0 +1,14 @@ +import { UserContext } from './teaql-ts'; +import { Q } from './generated/Q'; +import { Platform } from './generated/models/Platform'; + +/** @internal Generated typed Mutation bootstrap; never call from application code. */ +export async function ensureGeneratedBootstrap(context: UserContext): Promise { + let domainRoot = (await Q.platforms().withIdIs('1').comment('what: locate generated Domain Root').purpose('why: idempotent runtime bootstrap').executeForList(context))[0]; + if (!domainRoot) { + domainRoot = Platform.teaqlBootstrapNew('1'); + domainRoot.updateName("Trace Chain Verification"); + await domainRoot.auditAs('create generated Domain Root Platform').save(context); + } + context.withActiveRoot({ entity: 'Platform', id: domainRoot.id! }); +} diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated-search-models.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated-search-models.ts new file mode 100644 index 0000000000000000000000000000000000000000..a59e0d1620cbf89429accd6b89e48ec64c3b3183 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated-search-models.ts @@ -0,0 +1,128 @@ +import type { GeneratedSearchModel } from "teaql-ts"; + +/** Generated trusted naming/type metadata. No query execution or authorization lives here. */ +export const GENERATED_SEARCH_MODELS: Readonly> = Object.freeze({ + "Platform": Object.freeze({ + fields: Object.freeze({ + "id": "integer" as const, + "name": "string" as const, + "version": "integer" as const, + }), + queryFields: Object.freeze({ + "id": "id", + "name": "name", + "version": "version", + }), + relations: Object.freeze({ + "customerOrderList": "CustomerOrder", + }), + }), + "CustomerOrder": Object.freeze({ + fields: Object.freeze({ + "id": "integer" as const, + "platform": "integer" as const, + "order_number": "string" as const, + "orderNumber": "string" as const, + "description": "string" as const, + "version": "integer" as const, + }), + queryFields: Object.freeze({ + "id": "id", + "platform": "platform", + "order_number": "orderNumber", + "orderNumber": "orderNumber", + "description": "description", + "version": "version", + }), + relations: Object.freeze({ + "platform": "Platform", + "orderItemList": "OrderItem", + "paymentList": "Payment", + "shipmentList": "Shipment", + }), + }), + "OrderItem": Object.freeze({ + fields: Object.freeze({ + "id": "integer" as const, + "customer_order": "integer" as const, + "customerOrder": "integer" as const, + "name": "string" as const, + "version": "integer" as const, + }), + queryFields: Object.freeze({ + "id": "id", + "customer_order": "customerOrder", + "customerOrder": "customerOrder", + "name": "name", + "version": "version", + }), + relations: Object.freeze({ + "customer_order": "CustomerOrder", + "customerOrder": "CustomerOrder", + }), + }), + "Payment": Object.freeze({ + fields: Object.freeze({ + "id": "integer" as const, + "customer_order": "integer" as const, + "customerOrder": "integer" as const, + "reference_code": "string" as const, + "referenceCode": "string" as const, + "version": "integer" as const, + }), + queryFields: Object.freeze({ + "id": "id", + "customer_order": "customerOrder", + "customerOrder": "customerOrder", + "reference_code": "referenceCode", + "referenceCode": "referenceCode", + "version": "version", + }), + relations: Object.freeze({ + "customer_order": "CustomerOrder", + "customerOrder": "CustomerOrder", + "paymentAttemptList": "PaymentAttempt", + }), + }), + "PaymentAttempt": Object.freeze({ + fields: Object.freeze({ + "id": "integer" as const, + "payment": "integer" as const, + "reference_code": "string" as const, + "referenceCode": "string" as const, + "version": "integer" as const, + }), + queryFields: Object.freeze({ + "id": "id", + "payment": "payment", + "reference_code": "referenceCode", + "referenceCode": "referenceCode", + "version": "version", + }), + relations: Object.freeze({ + "payment": "Payment", + }), + }), + "Shipment": Object.freeze({ + fields: Object.freeze({ + "id": "integer" as const, + "customer_order": "integer" as const, + "customerOrder": "integer" as const, + "reference_code": "string" as const, + "referenceCode": "string" as const, + "version": "integer" as const, + }), + queryFields: Object.freeze({ + "id": "id", + "customer_order": "customerOrder", + "customerOrder": "customerOrder", + "reference_code": "referenceCode", + "referenceCode": "referenceCode", + "version": "version", + }), + relations: Object.freeze({ + "customer_order": "CustomerOrder", + "customerOrder": "CustomerOrder", + }), + }), +}); \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated/E.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated/E.ts new file mode 100644 index 0000000000000000000000000000000000000000..aa0cb8c865ed5c266c3cd868299a5fa32867e5a8 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated/E.ts @@ -0,0 +1,893 @@ +import { Platform } from './models/Platform'; +import { CustomerOrder } from './models/CustomerOrder'; +import { OrderItem } from './models/OrderItem'; +import { Payment } from './models/Payment'; +import { PaymentAttempt } from './models/PaymentAttempt'; +import { Shipment } from './models/Shipment'; + +export class TeaQLNotLoadedError extends Error { + readonly details: Record; + + constructor( + readonly root: string, + readonly accessPath: string, + readonly breakPoint: string, + ) { + const suggestedFix = `select${breakPoint.charAt(0).toUpperCase()}${breakPoint.slice(1)}(...)`; + const details = { + error: 'TeaQLNotLoadedError', + root, + accessPath: accessPath.split('.'), + breakPoint, + missingPreload: [breakPoint], + suggestedFix, + severity: 'error', + humanMessage: `访问 ${root}.${accessPath} 时缺少预加载。请在查询中加入 ${suggestedFix}`, + }; + super(JSON.stringify(details)); + this.name = 'TeaQLNotLoadedError'; + this.details = details; + } +} + +function expressionPath(prefix: string, field: string): string { + return prefix ? `${prefix}.${field}` : field; +} + +export class ValueExpression { + constructor( + private readonly value: T | null | undefined, + private readonly present = true, + private readonly notLoaded?: TeaQLNotLoadedError, + ) {} + + static missing(): ValueExpression { + return new ValueExpression(undefined, false); + } + + static notLoaded(error: TeaQLNotLoadedError): ValueExpression { + return new ValueExpression(undefined, false, error); + } + + eval(): T | null | undefined { + if (this.notLoaded) throw this.notLoaded; + return this.present ? this.value : undefined; + } + + isPresent(): boolean { + if (this.notLoaded) throw this.notLoaded; + return this.present; + } + + orElse(fallback: T): T { + const value = this.eval(); + return this.present && value != null ? value : fallback; + } +} + +export class PlatformExpression { + constructor( + private readonly value: Platform | null | undefined, + private readonly root = 'Platform(null)', + private readonly path = '', + private readonly notLoaded?: TeaQLNotLoadedError, + ) {} + + eval(): Platform | null | undefined { + if (this.notLoaded) throw this.notLoaded; + return this.value; + } + + id(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'id'); + if (!this.value.isLoaded('id')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'id'), + ); + } + return new ValueExpression(this.value.id); + } + + name(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'name'); + if (!this.value.isLoaded('name')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'name'), + ); + } + return new ValueExpression(this.value.name); + } + + version(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'version'); + if (!this.value.isLoaded('version')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'version'), + ); + } + return new ValueExpression(this.value.version); + } + + + + customerOrderList(): CustomerOrderListExpression { + const path = expressionPath(this.path, 'customerOrderList'); + if (this.notLoaded) { + return new CustomerOrderListExpression([], this.root, path, false, this.notLoaded); + } + if (!this.value) return CustomerOrderListExpression.missing(this.root, path); + if (!this.value.isLoaded('customerOrderList')) { + return new CustomerOrderListExpression([], this.root, path, false, + new TeaQLNotLoadedError(this.root, path, 'customerOrderList')); + } + return new CustomerOrderListExpression( + this.value.customerOrderList(), this.root, path, + ); + } +} + +export class CustomerOrderExpression { + constructor( + private readonly value: CustomerOrder | null | undefined, + private readonly root = 'CustomerOrder(null)', + private readonly path = '', + private readonly notLoaded?: TeaQLNotLoadedError, + ) {} + + eval(): CustomerOrder | null | undefined { + if (this.notLoaded) throw this.notLoaded; + return this.value; + } + + id(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'id'); + if (!this.value.isLoaded('id')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'id'), + ); + } + return new ValueExpression(this.value.id); + } + + orderNumber(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'orderNumber'); + if (!this.value.isLoaded('orderNumber')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'orderNumber'), + ); + } + return new ValueExpression(this.value.orderNumber); + } + + description(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'description'); + if (!this.value.isLoaded('description')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'description'), + ); + } + return new ValueExpression(this.value.description); + } + + version(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'version'); + if (!this.value.isLoaded('version')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'version'), + ); + } + return new ValueExpression(this.value.version); + } + + platformId(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'platform'); + if (!this.value.isLoaded('platform')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'platform'), + ); + } + const reference = this.value.platform; + return new ValueExpression( + reference instanceof Platform + ? reference.id : reference as string | number | undefined, + ); + } + + platform(): PlatformExpression { + const path = expressionPath(this.path, 'platform'); + if (this.notLoaded) { + return new PlatformExpression(undefined, this.root, path, this.notLoaded); + } + if (!this.value) return new PlatformExpression(undefined, this.root, path); + if (!this.value.isLoaded('platform')) { + return new PlatformExpression(undefined, this.root, path, + new TeaQLNotLoadedError(this.root, path, 'platform')); + } + const relation = this.value.platform; + const target = relation == null + ? undefined + : relation instanceof Platform + ? relation + : Platform.fromRecord({ id: relation }); + return new PlatformExpression(target, this.root, path); + } + + orderItemList(): OrderItemListExpression { + const path = expressionPath(this.path, 'orderItemList'); + if (this.notLoaded) { + return new OrderItemListExpression([], this.root, path, false, this.notLoaded); + } + if (!this.value) return OrderItemListExpression.missing(this.root, path); + if (!this.value.isLoaded('orderItemList')) { + return new OrderItemListExpression([], this.root, path, false, + new TeaQLNotLoadedError(this.root, path, 'orderItemList')); + } + return new OrderItemListExpression( + this.value.orderItemList(), this.root, path, + ); + } + + paymentList(): PaymentListExpression { + const path = expressionPath(this.path, 'paymentList'); + if (this.notLoaded) { + return new PaymentListExpression([], this.root, path, false, this.notLoaded); + } + if (!this.value) return PaymentListExpression.missing(this.root, path); + if (!this.value.isLoaded('paymentList')) { + return new PaymentListExpression([], this.root, path, false, + new TeaQLNotLoadedError(this.root, path, 'paymentList')); + } + return new PaymentListExpression( + this.value.paymentList(), this.root, path, + ); + } + + shipmentList(): ShipmentListExpression { + const path = expressionPath(this.path, 'shipmentList'); + if (this.notLoaded) { + return new ShipmentListExpression([], this.root, path, false, this.notLoaded); + } + if (!this.value) return ShipmentListExpression.missing(this.root, path); + if (!this.value.isLoaded('shipmentList')) { + return new ShipmentListExpression([], this.root, path, false, + new TeaQLNotLoadedError(this.root, path, 'shipmentList')); + } + return new ShipmentListExpression( + this.value.shipmentList(), this.root, path, + ); + } +} + +export class OrderItemExpression { + constructor( + private readonly value: OrderItem | null | undefined, + private readonly root = 'OrderItem(null)', + private readonly path = '', + private readonly notLoaded?: TeaQLNotLoadedError, + ) {} + + eval(): OrderItem | null | undefined { + if (this.notLoaded) throw this.notLoaded; + return this.value; + } + + id(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'id'); + if (!this.value.isLoaded('id')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'id'), + ); + } + return new ValueExpression(this.value.id); + } + + name(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'name'); + if (!this.value.isLoaded('name')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'name'), + ); + } + return new ValueExpression(this.value.name); + } + + version(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'version'); + if (!this.value.isLoaded('version')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'version'), + ); + } + return new ValueExpression(this.value.version); + } + + customerOrderId(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'customerOrder'); + if (!this.value.isLoaded('customerOrder')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'customerOrder'), + ); + } + const reference = this.value.customerOrder; + return new ValueExpression( + reference instanceof CustomerOrder + ? reference.id : reference as string | number | undefined, + ); + } + + customerOrder(): CustomerOrderExpression { + const path = expressionPath(this.path, 'customerOrder'); + if (this.notLoaded) { + return new CustomerOrderExpression(undefined, this.root, path, this.notLoaded); + } + if (!this.value) return new CustomerOrderExpression(undefined, this.root, path); + if (!this.value.isLoaded('customerOrder')) { + return new CustomerOrderExpression(undefined, this.root, path, + new TeaQLNotLoadedError(this.root, path, 'customerOrder')); + } + const relation = this.value.customerOrder; + const target = relation == null + ? undefined + : relation instanceof CustomerOrder + ? relation + : CustomerOrder.fromRecord({ id: relation }); + return new CustomerOrderExpression(target, this.root, path); + } + +} + +export class PaymentExpression { + constructor( + private readonly value: Payment | null | undefined, + private readonly root = 'Payment(null)', + private readonly path = '', + private readonly notLoaded?: TeaQLNotLoadedError, + ) {} + + eval(): Payment | null | undefined { + if (this.notLoaded) throw this.notLoaded; + return this.value; + } + + id(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'id'); + if (!this.value.isLoaded('id')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'id'), + ); + } + return new ValueExpression(this.value.id); + } + + referenceCode(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'referenceCode'); + if (!this.value.isLoaded('referenceCode')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'referenceCode'), + ); + } + return new ValueExpression(this.value.referenceCode); + } + + version(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'version'); + if (!this.value.isLoaded('version')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'version'), + ); + } + return new ValueExpression(this.value.version); + } + + customerOrderId(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'customerOrder'); + if (!this.value.isLoaded('customerOrder')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'customerOrder'), + ); + } + const reference = this.value.customerOrder; + return new ValueExpression( + reference instanceof CustomerOrder + ? reference.id : reference as string | number | undefined, + ); + } + + customerOrder(): CustomerOrderExpression { + const path = expressionPath(this.path, 'customerOrder'); + if (this.notLoaded) { + return new CustomerOrderExpression(undefined, this.root, path, this.notLoaded); + } + if (!this.value) return new CustomerOrderExpression(undefined, this.root, path); + if (!this.value.isLoaded('customerOrder')) { + return new CustomerOrderExpression(undefined, this.root, path, + new TeaQLNotLoadedError(this.root, path, 'customerOrder')); + } + const relation = this.value.customerOrder; + const target = relation == null + ? undefined + : relation instanceof CustomerOrder + ? relation + : CustomerOrder.fromRecord({ id: relation }); + return new CustomerOrderExpression(target, this.root, path); + } + + paymentAttemptList(): PaymentAttemptListExpression { + const path = expressionPath(this.path, 'paymentAttemptList'); + if (this.notLoaded) { + return new PaymentAttemptListExpression([], this.root, path, false, this.notLoaded); + } + if (!this.value) return PaymentAttemptListExpression.missing(this.root, path); + if (!this.value.isLoaded('paymentAttemptList')) { + return new PaymentAttemptListExpression([], this.root, path, false, + new TeaQLNotLoadedError(this.root, path, 'paymentAttemptList')); + } + return new PaymentAttemptListExpression( + this.value.paymentAttemptList(), this.root, path, + ); + } +} + +export class PaymentAttemptExpression { + constructor( + private readonly value: PaymentAttempt | null | undefined, + private readonly root = 'PaymentAttempt(null)', + private readonly path = '', + private readonly notLoaded?: TeaQLNotLoadedError, + ) {} + + eval(): PaymentAttempt | null | undefined { + if (this.notLoaded) throw this.notLoaded; + return this.value; + } + + id(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'id'); + if (!this.value.isLoaded('id')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'id'), + ); + } + return new ValueExpression(this.value.id); + } + + referenceCode(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'referenceCode'); + if (!this.value.isLoaded('referenceCode')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'referenceCode'), + ); + } + return new ValueExpression(this.value.referenceCode); + } + + version(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'version'); + if (!this.value.isLoaded('version')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'version'), + ); + } + return new ValueExpression(this.value.version); + } + + paymentId(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'payment'); + if (!this.value.isLoaded('payment')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'payment'), + ); + } + const reference = this.value.payment; + return new ValueExpression( + reference instanceof Payment + ? reference.id : reference as string | number | undefined, + ); + } + + payment(): PaymentExpression { + const path = expressionPath(this.path, 'payment'); + if (this.notLoaded) { + return new PaymentExpression(undefined, this.root, path, this.notLoaded); + } + if (!this.value) return new PaymentExpression(undefined, this.root, path); + if (!this.value.isLoaded('payment')) { + return new PaymentExpression(undefined, this.root, path, + new TeaQLNotLoadedError(this.root, path, 'payment')); + } + const relation = this.value.payment; + const target = relation == null + ? undefined + : relation instanceof Payment + ? relation + : Payment.fromRecord({ id: relation }); + return new PaymentExpression(target, this.root, path); + } + +} + +export class ShipmentExpression { + constructor( + private readonly value: Shipment | null | undefined, + private readonly root = 'Shipment(null)', + private readonly path = '', + private readonly notLoaded?: TeaQLNotLoadedError, + ) {} + + eval(): Shipment | null | undefined { + if (this.notLoaded) throw this.notLoaded; + return this.value; + } + + id(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'id'); + if (!this.value.isLoaded('id')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'id'), + ); + } + return new ValueExpression(this.value.id); + } + + referenceCode(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'referenceCode'); + if (!this.value.isLoaded('referenceCode')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'referenceCode'), + ); + } + return new ValueExpression(this.value.referenceCode); + } + + version(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'version'); + if (!this.value.isLoaded('version')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'version'), + ); + } + return new ValueExpression(this.value.version); + } + + customerOrderId(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + if (!this.value) return ValueExpression.missing(); + const path = expressionPath(this.path, 'customerOrder'); + if (!this.value.isLoaded('customerOrder')) { + return ValueExpression.notLoaded( + new TeaQLNotLoadedError(this.root, path, 'customerOrder'), + ); + } + const reference = this.value.customerOrder; + return new ValueExpression( + reference instanceof CustomerOrder + ? reference.id : reference as string | number | undefined, + ); + } + + customerOrder(): CustomerOrderExpression { + const path = expressionPath(this.path, 'customerOrder'); + if (this.notLoaded) { + return new CustomerOrderExpression(undefined, this.root, path, this.notLoaded); + } + if (!this.value) return new CustomerOrderExpression(undefined, this.root, path); + if (!this.value.isLoaded('customerOrder')) { + return new CustomerOrderExpression(undefined, this.root, path, + new TeaQLNotLoadedError(this.root, path, 'customerOrder')); + } + const relation = this.value.customerOrder; + const target = relation == null + ? undefined + : relation instanceof CustomerOrder + ? relation + : CustomerOrder.fromRecord({ id: relation }); + return new CustomerOrderExpression(target, this.root, path); + } + +} + +export class PlatformListExpression { + constructor( + private readonly items: readonly Platform[], + private readonly root = 'Platform(null)', + private readonly path = '', + private readonly present = true, + private readonly notLoaded?: TeaQLNotLoadedError, + ) {} + + static missing(root?: string, path = ''): PlatformListExpression { + return new PlatformListExpression([], root, path, false); + } + + size(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + return this.present + ? new ValueExpression(this.items.length) + : ValueExpression.missing(); + } + + first(): PlatformExpression { + return this.get(0); + } + + get(index: number): PlatformExpression { + const itemPath = expressionPath(this.path, `get(${index})`); + if (this.notLoaded) { + return new PlatformExpression(undefined, this.root, itemPath, this.notLoaded); + } + return !this.present || index < 0 || index >= this.items.length + ? new PlatformExpression(undefined, this.root, itemPath) + : new PlatformExpression(this.items[index], this.root, itemPath); + } +} + +export class CustomerOrderListExpression { + constructor( + private readonly items: readonly CustomerOrder[], + private readonly root = 'CustomerOrder(null)', + private readonly path = '', + private readonly present = true, + private readonly notLoaded?: TeaQLNotLoadedError, + ) {} + + static missing(root?: string, path = ''): CustomerOrderListExpression { + return new CustomerOrderListExpression([], root, path, false); + } + + size(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + return this.present + ? new ValueExpression(this.items.length) + : ValueExpression.missing(); + } + + first(): CustomerOrderExpression { + return this.get(0); + } + + get(index: number): CustomerOrderExpression { + const itemPath = expressionPath(this.path, `get(${index})`); + if (this.notLoaded) { + return new CustomerOrderExpression(undefined, this.root, itemPath, this.notLoaded); + } + return !this.present || index < 0 || index >= this.items.length + ? new CustomerOrderExpression(undefined, this.root, itemPath) + : new CustomerOrderExpression(this.items[index], this.root, itemPath); + } +} + +export class OrderItemListExpression { + constructor( + private readonly items: readonly OrderItem[], + private readonly root = 'OrderItem(null)', + private readonly path = '', + private readonly present = true, + private readonly notLoaded?: TeaQLNotLoadedError, + ) {} + + static missing(root?: string, path = ''): OrderItemListExpression { + return new OrderItemListExpression([], root, path, false); + } + + size(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + return this.present + ? new ValueExpression(this.items.length) + : ValueExpression.missing(); + } + + first(): OrderItemExpression { + return this.get(0); + } + + get(index: number): OrderItemExpression { + const itemPath = expressionPath(this.path, `get(${index})`); + if (this.notLoaded) { + return new OrderItemExpression(undefined, this.root, itemPath, this.notLoaded); + } + return !this.present || index < 0 || index >= this.items.length + ? new OrderItemExpression(undefined, this.root, itemPath) + : new OrderItemExpression(this.items[index], this.root, itemPath); + } +} + +export class PaymentListExpression { + constructor( + private readonly items: readonly Payment[], + private readonly root = 'Payment(null)', + private readonly path = '', + private readonly present = true, + private readonly notLoaded?: TeaQLNotLoadedError, + ) {} + + static missing(root?: string, path = ''): PaymentListExpression { + return new PaymentListExpression([], root, path, false); + } + + size(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + return this.present + ? new ValueExpression(this.items.length) + : ValueExpression.missing(); + } + + first(): PaymentExpression { + return this.get(0); + } + + get(index: number): PaymentExpression { + const itemPath = expressionPath(this.path, `get(${index})`); + if (this.notLoaded) { + return new PaymentExpression(undefined, this.root, itemPath, this.notLoaded); + } + return !this.present || index < 0 || index >= this.items.length + ? new PaymentExpression(undefined, this.root, itemPath) + : new PaymentExpression(this.items[index], this.root, itemPath); + } +} + +export class PaymentAttemptListExpression { + constructor( + private readonly items: readonly PaymentAttempt[], + private readonly root = 'PaymentAttempt(null)', + private readonly path = '', + private readonly present = true, + private readonly notLoaded?: TeaQLNotLoadedError, + ) {} + + static missing(root?: string, path = ''): PaymentAttemptListExpression { + return new PaymentAttemptListExpression([], root, path, false); + } + + size(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + return this.present + ? new ValueExpression(this.items.length) + : ValueExpression.missing(); + } + + first(): PaymentAttemptExpression { + return this.get(0); + } + + get(index: number): PaymentAttemptExpression { + const itemPath = expressionPath(this.path, `get(${index})`); + if (this.notLoaded) { + return new PaymentAttemptExpression(undefined, this.root, itemPath, this.notLoaded); + } + return !this.present || index < 0 || index >= this.items.length + ? new PaymentAttemptExpression(undefined, this.root, itemPath) + : new PaymentAttemptExpression(this.items[index], this.root, itemPath); + } +} + +export class ShipmentListExpression { + constructor( + private readonly items: readonly Shipment[], + private readonly root = 'Shipment(null)', + private readonly path = '', + private readonly present = true, + private readonly notLoaded?: TeaQLNotLoadedError, + ) {} + + static missing(root?: string, path = ''): ShipmentListExpression { + return new ShipmentListExpression([], root, path, false); + } + + size(): ValueExpression { + if (this.notLoaded) return ValueExpression.notLoaded(this.notLoaded); + return this.present + ? new ValueExpression(this.items.length) + : ValueExpression.missing(); + } + + first(): ShipmentExpression { + return this.get(0); + } + + get(index: number): ShipmentExpression { + const itemPath = expressionPath(this.path, `get(${index})`); + if (this.notLoaded) { + return new ShipmentExpression(undefined, this.root, itemPath, this.notLoaded); + } + return !this.present || index < 0 || index >= this.items.length + ? new ShipmentExpression(undefined, this.root, itemPath) + : new ShipmentExpression(this.items[index], this.root, itemPath); + } +} + +export class E { + static platform( + value: Platform | null | undefined, + ): PlatformExpression { + return new PlatformExpression( + value, `Platform(id=${value?.id ?? 'null'})`, + ); + } + + static customerOrder( + value: CustomerOrder | null | undefined, + ): CustomerOrderExpression { + return new CustomerOrderExpression( + value, `CustomerOrder(id=${value?.id ?? 'null'})`, + ); + } + + static orderItem( + value: OrderItem | null | undefined, + ): OrderItemExpression { + return new OrderItemExpression( + value, `OrderItem(id=${value?.id ?? 'null'})`, + ); + } + + static payment( + value: Payment | null | undefined, + ): PaymentExpression { + return new PaymentExpression( + value, `Payment(id=${value?.id ?? 'null'})`, + ); + } + + static paymentAttempt( + value: PaymentAttempt | null | undefined, + ): PaymentAttemptExpression { + return new PaymentAttemptExpression( + value, `PaymentAttempt(id=${value?.id ?? 'null'})`, + ); + } + + static shipment( + value: Shipment | null | undefined, + ): ShipmentExpression { + return new ShipmentExpression( + value, `Shipment(id=${value?.id ?? 'null'})`, + ); + } +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated/Q.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated/Q.ts new file mode 100644 index 0000000000000000000000000000000000000000..802e9f10bb78c1e6c929d1b448d7ca925e1815f4 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated/Q.ts @@ -0,0 +1,52 @@ +import { TeaQLClient } from '../teaql-ts'; +import { PlatformRequest } from './requests/PlatformRequest'; +import { CustomerOrderRequest } from './requests/CustomerOrderRequest'; +import { OrderItemRequest } from './requests/OrderItemRequest'; +import { PaymentRequest } from './requests/PaymentRequest'; +import { PaymentAttemptRequest } from './requests/PaymentAttemptRequest'; +import { ShipmentRequest } from './requests/ShipmentRequest'; + +export class Q { + static platforms(): PlatformRequest { + return new PlatformRequest(false); + } + + static platformsWithMinimalFields(): PlatformRequest { + return new PlatformRequest(true); + } + static customerOrders(): CustomerOrderRequest { + return new CustomerOrderRequest(false); + } + + static customerOrdersWithMinimalFields(): CustomerOrderRequest { + return new CustomerOrderRequest(true); + } + static orderItems(): OrderItemRequest { + return new OrderItemRequest(false); + } + + static orderItemsWithMinimalFields(): OrderItemRequest { + return new OrderItemRequest(true); + } + static payments(): PaymentRequest { + return new PaymentRequest(false); + } + + static paymentsWithMinimalFields(): PaymentRequest { + return new PaymentRequest(true); + } + static paymentAttempts(): PaymentAttemptRequest { + return new PaymentAttemptRequest(false); + } + + static paymentAttemptsWithMinimalFields(): PaymentAttemptRequest { + return new PaymentAttemptRequest(true); + } + static shipments(): ShipmentRequest { + return new ShipmentRequest(false); + } + + static shipmentsWithMinimalFields(): ShipmentRequest { + return new ShipmentRequest(true); + } +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated/models/CustomerOrder.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated/models/CustomerOrder.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab40b8d9609c4d1838571b601bc9782d52bcbd42 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated/models/CustomerOrder.ts @@ -0,0 +1,407 @@ +import { CheckException, EntityKey, EntityRoot, GraphMutationSession, LoadedScalarSnapshot, MutationIntent, MutationTraceScope, ObjectLocation, SmartList, TeaQLDataService, UserContext } from '../../teaql-ts'; +import { Platform } from './Platform'; +import { OrderItem } from './OrderItem'; +import { Payment } from './Payment'; +import { Shipment } from './Shipment'; + +export class CustomerOrder { + private static teaqlTemporaryId = 0; + id?: string; + platform?: Platform | string; + orderNumber?: string; + description?: string; + version?: number; + + constructor(init?: Partial) { + Object.defineProperty(this, "_action", { value: init && init.id ? "Update" : "Create", writable: true, enumerable: false }); + Object.defineProperty(this, "_comment", { value: undefined, writable: true, enumerable: false }); + Object.defineProperty(this, "_fullyLoaded", { value: !init || !init.id, writable: true, enumerable: false }); + Object.defineProperty(this, "_loadedFields", { value: new Set(Object.keys(init || {})), writable: true, enumerable: false }); + Object.defineProperty(this, "_root", { value: new EntityRoot(), writable: true, enumerable: false }); + Object.defineProperty(this, "_ledgerId", { value: init?.id ?? --CustomerOrder.teaqlTemporaryId, writable: true, enumerable: false }); + Object.defineProperty(this, "_orderItemList", { value: init?.id ? SmartList.empty() : new SmartList(), writable: true, enumerable: false }); + Object.defineProperty(this, "_paymentList", { value: init?.id ? SmartList.empty() : new SmartList(), writable: true, enumerable: false }); + Object.defineProperty(this, "_shipmentList", { value: init?.id ? SmartList.empty() : new SmartList(), writable: true, enumerable: false }); + if (init) { + const fields: Record = { ...(init as Record) }; + delete fields["orderItemList"]; + delete fields["paymentList"]; + delete fields["shipmentList"]; + Object.assign(this, fields); + if ((init as any).platform && typeof (init as any).platform === "object") { + this.platform = (init as any).platform instanceof Platform + ? (init as any).platform + : Platform.fromRecord((init as any).platform); + this.markLoaded("platform"); + } + if (Array.isArray((init as any).orderItemList)) { + const selected = (init as any).orderItemList; + (this as any)._orderItemList = new SmartList(selected.map( + (child: unknown) => child instanceof OrderItem + ? child + : OrderItem.fromRecord(child as Record), + ), { totalCount: selected.totalCount, aggregations: selected.aggregations, + summary: selected.summary, facets: selected.facets, isLoaded: selected.isLoaded ?? true }); + this.markLoaded("orderItemList"); + } + if (Array.isArray((init as any).paymentList)) { + const selected = (init as any).paymentList; + (this as any)._paymentList = new SmartList(selected.map( + (child: unknown) => child instanceof Payment + ? child + : Payment.fromRecord(child as Record), + ), { totalCount: selected.totalCount, aggregations: selected.aggregations, + summary: selected.summary, facets: selected.facets, isLoaded: selected.isLoaded ?? true }); + this.markLoaded("paymentList"); + } + if (Array.isArray((init as any).shipmentList)) { + const selected = (init as any).shipmentList; + (this as any)._shipmentList = new SmartList(selected.map( + (child: unknown) => child instanceof Shipment + ? child + : Shipment.fromRecord(child as Record), + ), { totalCount: selected.totalCount, aggregations: selected.aggregations, + summary: selected.summary, facets: selected.facets, isLoaded: selected.isLoaded ?? true }); + this.markLoaded("shipmentList"); + } + } + const key = this.teaqlEntityKey(); + if ((this as any)._action === "Create") (this as any)._root.markAsNew(key); + else if ((this as any).version !== undefined) (this as any)._root.setOriginalVersion(key, Number((this as any).version)); + LoadedScalarSnapshot.captureFor(this, this.teaqlMutationPayload()); + } + + private teaqlEntityKey(): EntityKey { return { entity: "CustomerOrder", id: (this as any)._ledgerId }; } + private teaqlAttachRoot(root: EntityRoot): this { + if ((this as any)._root !== root) { + root.mergeEntityFrom((this as any)._root, this.teaqlEntityKey()); + (this as any)._root = root; + } + for (const child of (this as any)._orderItemList) child.teaqlAttachRoot(root); + for (const child of (this as any)._paymentList) child.teaqlAttachRoot(root); + for (const child of (this as any)._shipmentList) child.teaqlAttachRoot(root); + return this; + } + + static fromRecord(record: Record, root?: EntityRoot): CustomerOrder { + const entity = new CustomerOrder(record as Partial); + return root ? entity.teaqlAttachRoot(root) : entity; + } + + isLoaded(field: string): boolean { + return (this as any)._fullyLoaded || (this as any)._loadedFields.has(field); + } + + markLoaded(...fields: string[]): this { + for (const field of fields) (this as any)._loadedFields.add(field); + return this; + } + + markLoadedOnly(...fields: string[]): this { + (this as any)._fullyLoaded = false; + (this as any)._loadedFields = new Set(fields); + return this; + } + + static refer(id: string | number): CustomerOrder { + return new CustomerOrder({ id: String(id) } as Partial); + } + + /** @internal Generated Runtime Module fixed-ID construction capability. */ + static teaqlBootstrapNew(id: string | number): CustomerOrder { + return new CustomerOrder().updateId(String(id)); + } + + markForDeletion(): this { + (this as any)._action = "Delete"; + (this as any)._root.markAsDeleted(this.teaqlEntityKey()); + return this; + } + + auditAs(comment: string): this { + new MutationIntent(comment); + (this as any)._comment = comment; + return this; + } + + async save(context: UserContext): Promise { + const service = context.requireResource("dataService"); + return service.executeGraphSave(new MutationIntent((this as any)._comment), async (graph) => { + this.teaqlPreflightGraph(context, service, graph); + return this.teaqlSaveWithinGraph(context, service, graph); + }); + } + + /** @internal Validates and fixes the complete graph before its first mutation. */ + teaqlPreflightGraph(context: UserContext, service: TeaQLDataService, graph: GraphMutationSession, parent?: MutationTraceScope): void { + if (!parent) new MutationIntent((this as any)._comment); + const action = (this as any)._action; + const pending = action !== "Update" || (this as any)._root.hasPending(this.teaqlEntityKey()); + if (action === "Update" && pending) { + const notLoaded = [{ member: "id", canonical: "id" }, { member: "platform", canonical: "platform" }, { member: "orderNumber", canonical: "order_number" }, { member: "description", canonical: "description" }, { member: "version", canonical: "version" }] + .find(field => !this.isLoaded(field.member)); + if (notLoaded) { + throw new CheckException([{ + ruleId: "invalid_type", + location: ObjectLocation.property(notLoaded.canonical), + message: "Mutation requires a fully loaded entity", + }]); + } + } + const request = graph.request({ + entity: "CustomerOrder", action, + payload: action === "Update" + ? (this as any)._root.change(this.teaqlEntityKey()) + : this.teaqlMutationPayload(), + id: (this as any).id, version: (this as any).version, + comment: (this as any)._comment, + ledgerKey: this.teaqlEntityKey(), ledgerRoot: (this as any)._root, + }, parent, (this as any)._comment).withLoadedSnapshot(LoadedScalarSnapshot.forEntity(this) ?? new LoadedScalarSnapshot()); + if (pending) service.preflightMutation(request); + const scope = request.scopeFor(this.teaqlEntityKey()); + for (const [index, child] of (this as any)._orderItemList.entries()) { + child.teaqlAttachRoot((this as any)._root); + const currentParent = child.customerOrder; + if (this.id === undefined || !child.isLoaded("customerOrder") || (typeof currentParent === "object" && currentParent !== null + ? currentParent.id : currentParent) !== this.id) child.updateCustomerOrder(this); + try { child.teaqlPreflightGraph(context, service, graph, scope); } + catch (error) { + if (!(error instanceof CheckException)) throw error; + const prefix = ObjectLocation.property("order_item_list").index(index); + throw new CheckException(error.violations.map(violation => ({ + ...violation, location: violation.location.prefixedBy(prefix), + }))); + } + } + for (const [index, child] of (this as any)._paymentList.entries()) { + child.teaqlAttachRoot((this as any)._root); + const currentParent = child.customerOrder; + if (this.id === undefined || !child.isLoaded("customerOrder") || (typeof currentParent === "object" && currentParent !== null + ? currentParent.id : currentParent) !== this.id) child.updateCustomerOrder(this); + try { child.teaqlPreflightGraph(context, service, graph, scope); } + catch (error) { + if (!(error instanceof CheckException)) throw error; + const prefix = ObjectLocation.property("payment_list").index(index); + throw new CheckException(error.violations.map(violation => ({ + ...violation, location: violation.location.prefixedBy(prefix), + }))); + } + } + for (const [index, child] of (this as any)._shipmentList.entries()) { + child.teaqlAttachRoot((this as any)._root); + const currentParent = child.customerOrder; + if (this.id === undefined || !child.isLoaded("customerOrder") || (typeof currentParent === "object" && currentParent !== null + ? currentParent.id : currentParent) !== this.id) child.updateCustomerOrder(this); + try { child.teaqlPreflightGraph(context, service, graph, scope); } + catch (error) { + if (!(error instanceof CheckException)) throw error; + const prefix = ObjectLocation.property("shipment_list").index(index); + throw new CheckException(error.violations.map(violation => ({ + ...violation, location: violation.location.prefixedBy(prefix), + }))); + } + } + } + + /** @internal Used by generated relation cascades inside the root graph transaction. */ + async teaqlSaveWithinGraph(context: UserContext, service: TeaQLDataService, graph: GraphMutationSession, parent?: MutationTraceScope): Promise { + if (!parent) new MutationIntent((this as any)._comment); + const action = (this as any)._action; + const ledgerPayload = (this as any)._root.change(this.teaqlEntityKey()); + const mutation = { + entity: "CustomerOrder", + action: (this as any)._action, + payload: action === "Update" ? ledgerPayload : this.teaqlMutationPayload(), + id: (this as any).id, + version: (this as any).version, + comment: (this as any)._comment + ,ledgerKey: this.teaqlEntityKey() + ,ledgerRoot: (this as any)._root + }; + const request = graph.request(mutation, parent, (this as any)._comment) + .withLoadedSnapshot(LoadedScalarSnapshot.forEntity(this) ?? new LoadedScalarSnapshot()); + if (action === "Update" && !(this as any)._root.hasPending(this.teaqlEntityKey())) { + const scope = request.scopeFor(this.teaqlEntityKey()); + for (const [index, child] of (this as any)._orderItemList.entries()) { + child.teaqlAttachRoot((this as any)._root); + const currentParent = child.customerOrder; + if (this.id === undefined || !child.isLoaded("customerOrder") || (typeof currentParent === "object" && currentParent !== null + ? currentParent.id : currentParent) !== this.id) child.updateCustomerOrder(this); + try { await child.teaqlSaveWithinGraph(context, service, graph, scope); } + catch (error) { + if (!(error instanceof CheckException)) throw error; + const prefix = ObjectLocation.property("order_item_list").index(index); + throw new CheckException(error.violations.map(violation => ({ + ...violation, location: violation.location.prefixedBy(prefix), + }))); + } + } + for (const [index, child] of (this as any)._paymentList.entries()) { + child.teaqlAttachRoot((this as any)._root); + const currentParent = child.customerOrder; + if (this.id === undefined || !child.isLoaded("customerOrder") || (typeof currentParent === "object" && currentParent !== null + ? currentParent.id : currentParent) !== this.id) child.updateCustomerOrder(this); + try { await child.teaqlSaveWithinGraph(context, service, graph, scope); } + catch (error) { + if (!(error instanceof CheckException)) throw error; + const prefix = ObjectLocation.property("payment_list").index(index); + throw new CheckException(error.violations.map(violation => ({ + ...violation, location: violation.location.prefixedBy(prefix), + }))); + } + } + for (const [index, child] of (this as any)._shipmentList.entries()) { + child.teaqlAttachRoot((this as any)._root); + const currentParent = child.customerOrder; + if (this.id === undefined || !child.isLoaded("customerOrder") || (typeof currentParent === "object" && currentParent !== null + ? currentParent.id : currentParent) !== this.id) child.updateCustomerOrder(this); + try { await child.teaqlSaveWithinGraph(context, service, graph, scope); } + catch (error) { + if (!(error instanceof CheckException)) throw error; + const prefix = ObjectLocation.property("shipment_list").index(index); + throw new CheckException(error.violations.map(violation => ({ + ...violation, location: violation.location.prefixedBy(prefix), + }))); + } + } + return this; + } + const result = await service.executeMutation(request); + for (const [field, value] of Object.entries(mutation.payload as Record)) { + if (field !== "id" && field !== "version") (this as any)._root.set(this.teaqlEntityKey(), field, value); + } + if (!result.persistedRecord) { + throw new Error("Mutation did not return the authoritative persisted record"); + } + const rollbackState = { + payload: this.teaqlMutationPayload(), + ledgerId: (this as any)._ledgerId, + action: (this as any)._action, + loadedFields: new Set((this as any)._loadedFields), + fullyLoaded: (this as any)._fullyLoaded, + loadedSnapshot: LoadedScalarSnapshot.forEntity(this), + }; + const oldKey = this.teaqlEntityKey(); + Object.assign(this, result.persistedRecord); + LoadedScalarSnapshot.captureFor(this, this.teaqlMutationPayload()); + (this as any)._ledgerId = (this as any).id ?? (this as any)._ledgerId; + const newKey = this.teaqlEntityKey(); + (this as any)._root.rekey(oldKey, newKey); + service.afterGraphRollback(() => { + Object.assign(this, rollbackState.payload); + (this as any)._ledgerId = rollbackState.ledgerId; + (this as any)._action = rollbackState.action; + (this as any)._loadedFields = rollbackState.loadedFields; + (this as any)._fullyLoaded = rollbackState.fullyLoaded; + LoadedScalarSnapshot.restoreFor(this, rollbackState.loadedSnapshot); + (this as any)._root.rekey(newKey, oldKey); + }); + (this as any)._loadedFields = new Set(Object.keys(result.persistedRecord)); + (this as any)._fullyLoaded = false; + if (mutation.action !== "Delete") (this as any)._action = "Update"; + const scope = request.scopeFor(newKey); + for (const [index, child] of (this as any)._orderItemList.entries()) { + child.teaqlAttachRoot((this as any)._root); + const currentParent = child.customerOrder; + if (this.id === undefined || !child.isLoaded("customerOrder") || (typeof currentParent === "object" && currentParent !== null + ? currentParent.id : currentParent) !== this.id) child.updateCustomerOrder(this); + try { await child.teaqlSaveWithinGraph(context, service, graph, scope); } + catch (error) { + if (!(error instanceof CheckException)) throw error; + const prefix = ObjectLocation.property("order_item_list").index(index); + throw new CheckException(error.violations.map(violation => ({ + ...violation, location: violation.location.prefixedBy(prefix), + }))); + } + } + for (const [index, child] of (this as any)._paymentList.entries()) { + child.teaqlAttachRoot((this as any)._root); + const currentParent = child.customerOrder; + if (this.id === undefined || !child.isLoaded("customerOrder") || (typeof currentParent === "object" && currentParent !== null + ? currentParent.id : currentParent) !== this.id) child.updateCustomerOrder(this); + try { await child.teaqlSaveWithinGraph(context, service, graph, scope); } + catch (error) { + if (!(error instanceof CheckException)) throw error; + const prefix = ObjectLocation.property("payment_list").index(index); + throw new CheckException(error.violations.map(violation => ({ + ...violation, location: violation.location.prefixedBy(prefix), + }))); + } + } + for (const [index, child] of (this as any)._shipmentList.entries()) { + child.teaqlAttachRoot((this as any)._root); + const currentParent = child.customerOrder; + if (this.id === undefined || !child.isLoaded("customerOrder") || (typeof currentParent === "object" && currentParent !== null + ? currentParent.id : currentParent) !== this.id) child.updateCustomerOrder(this); + try { await child.teaqlSaveWithinGraph(context, service, graph, scope); } + catch (error) { + if (!(error instanceof CheckException)) throw error; + const prefix = ObjectLocation.property("shipment_list").index(index); + throw new CheckException(error.violations.map(violation => ({ + ...violation, location: violation.location.prefixedBy(prefix), + }))); + } + } + service.afterGraphCommit(() => { + (this as any)._root.clearEntity(newKey); + if ((this as any).version !== undefined) (this as any)._root.acceptCommittedVersion(newKey, Number((this as any).version)); + }); + return this; + } + + private teaqlMutationPayload(): Record { + return { + "id": this.id, + "platform": this.platform, + "order_number": this.orderNumber, + "description": this.description, + "version": this.version + }; + } + + updateId(value: string): this { + this.id = value; + this.markLoaded("id"); + (this as any)._root.set(this.teaqlEntityKey(), "id", value); + return this; + } + + updateOrderNumber(value: string): this { + this.orderNumber = value; + this.markLoaded("orderNumber"); + (this as any)._root.set(this.teaqlEntityKey(), "order_number", value); + return this; + } + + updateDescription(value: string): this { + this.description = value; + this.markLoaded("description"); + (this as any)._root.set(this.teaqlEntityKey(), "description", value); + return this; + } + + updateVersion(value: number): this { + this.version = value; + this.markLoaded("version"); + (this as any)._root.set(this.teaqlEntityKey(), "version", value); + return this; + } + updatePlatform(value: any): this { + this.platform = value?.id ?? value; + this.markLoaded("platform"); + (this as any)._root.set(this.teaqlEntityKey(), "platform", this.platform); + return this; + } + + orderItemList(): SmartList { + return (this as any)._orderItemList; + } + + paymentList(): SmartList { + return (this as any)._paymentList; + } + + shipmentList(): SmartList { + return (this as any)._shipmentList; + } +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated/models/OrderItem.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated/models/OrderItem.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a8d1bb31d27005773cbe1ff5aa1a43d70eb2c36 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated/models/OrderItem.ts @@ -0,0 +1,219 @@ +import { CheckException, EntityKey, EntityRoot, GraphMutationSession, LoadedScalarSnapshot, MutationIntent, MutationTraceScope, ObjectLocation, SmartList, TeaQLDataService, UserContext } from '../../teaql-ts'; +import { CustomerOrder } from './CustomerOrder'; + +export class OrderItem { + private static teaqlTemporaryId = 0; + id?: string; + customerOrder?: CustomerOrder | string; + name?: string; + version?: number; + + constructor(init?: Partial) { + Object.defineProperty(this, "_action", { value: init && init.id ? "Update" : "Create", writable: true, enumerable: false }); + Object.defineProperty(this, "_comment", { value: undefined, writable: true, enumerable: false }); + Object.defineProperty(this, "_fullyLoaded", { value: !init || !init.id, writable: true, enumerable: false }); + Object.defineProperty(this, "_loadedFields", { value: new Set(Object.keys(init || {})), writable: true, enumerable: false }); + Object.defineProperty(this, "_root", { value: new EntityRoot(), writable: true, enumerable: false }); + Object.defineProperty(this, "_ledgerId", { value: init?.id ?? --OrderItem.teaqlTemporaryId, writable: true, enumerable: false }); + if (init) { + const fields: Record = { ...(init as Record) }; + Object.assign(this, fields); + if ((init as any).customerOrder && typeof (init as any).customerOrder === "object") { + this.customerOrder = (init as any).customerOrder instanceof CustomerOrder + ? (init as any).customerOrder + : CustomerOrder.fromRecord((init as any).customerOrder); + this.markLoaded("customerOrder"); + } + } + const key = this.teaqlEntityKey(); + if ((this as any)._action === "Create") (this as any)._root.markAsNew(key); + else if ((this as any).version !== undefined) (this as any)._root.setOriginalVersion(key, Number((this as any).version)); + LoadedScalarSnapshot.captureFor(this, this.teaqlMutationPayload()); + } + + private teaqlEntityKey(): EntityKey { return { entity: "OrderItem", id: (this as any)._ledgerId }; } + private teaqlAttachRoot(root: EntityRoot): this { + if ((this as any)._root !== root) { + root.mergeEntityFrom((this as any)._root, this.teaqlEntityKey()); + (this as any)._root = root; + } + return this; + } + + static fromRecord(record: Record, root?: EntityRoot): OrderItem { + const entity = new OrderItem(record as Partial); + return root ? entity.teaqlAttachRoot(root) : entity; + } + + isLoaded(field: string): boolean { + return (this as any)._fullyLoaded || (this as any)._loadedFields.has(field); + } + + markLoaded(...fields: string[]): this { + for (const field of fields) (this as any)._loadedFields.add(field); + return this; + } + + markLoadedOnly(...fields: string[]): this { + (this as any)._fullyLoaded = false; + (this as any)._loadedFields = new Set(fields); + return this; + } + + static refer(id: string | number): OrderItem { + return new OrderItem({ id: String(id) } as Partial); + } + + /** @internal Generated Runtime Module fixed-ID construction capability. */ + static teaqlBootstrapNew(id: string | number): OrderItem { + return new OrderItem().updateId(String(id)); + } + + markForDeletion(): this { + (this as any)._action = "Delete"; + (this as any)._root.markAsDeleted(this.teaqlEntityKey()); + return this; + } + + auditAs(comment: string): this { + new MutationIntent(comment); + (this as any)._comment = comment; + return this; + } + + async save(context: UserContext): Promise { + const service = context.requireResource("dataService"); + return service.executeGraphSave(new MutationIntent((this as any)._comment), async (graph) => { + this.teaqlPreflightGraph(context, service, graph); + return this.teaqlSaveWithinGraph(context, service, graph); + }); + } + + /** @internal Validates and fixes the complete graph before its first mutation. */ + teaqlPreflightGraph(context: UserContext, service: TeaQLDataService, graph: GraphMutationSession, parent?: MutationTraceScope): void { + if (!parent) new MutationIntent((this as any)._comment); + const action = (this as any)._action; + const pending = action !== "Update" || (this as any)._root.hasPending(this.teaqlEntityKey()); + if (action === "Update" && pending) { + const notLoaded = [{ member: "id", canonical: "id" }, { member: "customerOrder", canonical: "customer_order" }, { member: "name", canonical: "name" }, { member: "version", canonical: "version" }] + .find(field => !this.isLoaded(field.member)); + if (notLoaded) { + throw new CheckException([{ + ruleId: "invalid_type", + location: ObjectLocation.property(notLoaded.canonical), + message: "Mutation requires a fully loaded entity", + }]); + } + } + const request = graph.request({ + entity: "OrderItem", action, + payload: action === "Update" + ? (this as any)._root.change(this.teaqlEntityKey()) + : this.teaqlMutationPayload(), + id: (this as any).id, version: (this as any).version, + comment: (this as any)._comment, + ledgerKey: this.teaqlEntityKey(), ledgerRoot: (this as any)._root, + }, parent, (this as any)._comment).withLoadedSnapshot(LoadedScalarSnapshot.forEntity(this) ?? new LoadedScalarSnapshot()); + if (pending) service.preflightMutation(request); + const scope = request.scopeFor(this.teaqlEntityKey()); + } + + /** @internal Used by generated relation cascades inside the root graph transaction. */ + async teaqlSaveWithinGraph(context: UserContext, service: TeaQLDataService, graph: GraphMutationSession, parent?: MutationTraceScope): Promise { + if (!parent) new MutationIntent((this as any)._comment); + const action = (this as any)._action; + const ledgerPayload = (this as any)._root.change(this.teaqlEntityKey()); + const mutation = { + entity: "OrderItem", + action: (this as any)._action, + payload: action === "Update" ? ledgerPayload : this.teaqlMutationPayload(), + id: (this as any).id, + version: (this as any).version, + comment: (this as any)._comment + ,ledgerKey: this.teaqlEntityKey() + ,ledgerRoot: (this as any)._root + }; + const request = graph.request(mutation, parent, (this as any)._comment) + .withLoadedSnapshot(LoadedScalarSnapshot.forEntity(this) ?? new LoadedScalarSnapshot()); + if (action === "Update" && !(this as any)._root.hasPending(this.teaqlEntityKey())) { + const scope = request.scopeFor(this.teaqlEntityKey()); + return this; + } + const result = await service.executeMutation(request); + for (const [field, value] of Object.entries(mutation.payload as Record)) { + if (field !== "id" && field !== "version") (this as any)._root.set(this.teaqlEntityKey(), field, value); + } + if (!result.persistedRecord) { + throw new Error("Mutation did not return the authoritative persisted record"); + } + const rollbackState = { + payload: this.teaqlMutationPayload(), + ledgerId: (this as any)._ledgerId, + action: (this as any)._action, + loadedFields: new Set((this as any)._loadedFields), + fullyLoaded: (this as any)._fullyLoaded, + loadedSnapshot: LoadedScalarSnapshot.forEntity(this), + }; + const oldKey = this.teaqlEntityKey(); + Object.assign(this, result.persistedRecord); + LoadedScalarSnapshot.captureFor(this, this.teaqlMutationPayload()); + (this as any)._ledgerId = (this as any).id ?? (this as any)._ledgerId; + const newKey = this.teaqlEntityKey(); + (this as any)._root.rekey(oldKey, newKey); + service.afterGraphRollback(() => { + Object.assign(this, rollbackState.payload); + (this as any)._ledgerId = rollbackState.ledgerId; + (this as any)._action = rollbackState.action; + (this as any)._loadedFields = rollbackState.loadedFields; + (this as any)._fullyLoaded = rollbackState.fullyLoaded; + LoadedScalarSnapshot.restoreFor(this, rollbackState.loadedSnapshot); + (this as any)._root.rekey(newKey, oldKey); + }); + (this as any)._loadedFields = new Set(Object.keys(result.persistedRecord)); + (this as any)._fullyLoaded = false; + if (mutation.action !== "Delete") (this as any)._action = "Update"; + const scope = request.scopeFor(newKey); + service.afterGraphCommit(() => { + (this as any)._root.clearEntity(newKey); + if ((this as any).version !== undefined) (this as any)._root.acceptCommittedVersion(newKey, Number((this as any).version)); + }); + return this; + } + + private teaqlMutationPayload(): Record { + return { + "id": this.id, + "customer_order": this.customerOrder, + "name": this.name, + "version": this.version + }; + } + + updateId(value: string): this { + this.id = value; + this.markLoaded("id"); + (this as any)._root.set(this.teaqlEntityKey(), "id", value); + return this; + } + + updateName(value: string): this { + this.name = value; + this.markLoaded("name"); + (this as any)._root.set(this.teaqlEntityKey(), "name", value); + return this; + } + + updateVersion(value: number): this { + this.version = value; + this.markLoaded("version"); + (this as any)._root.set(this.teaqlEntityKey(), "version", value); + return this; + } + updateCustomerOrder(value: any): this { + this.customerOrder = value?.id ?? value; + this.markLoaded("customerOrder"); + (this as any)._root.set(this.teaqlEntityKey(), "customer_order", this.customerOrder); + return this; + } + +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated/models/Payment.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated/models/Payment.ts new file mode 100644 index 0000000000000000000000000000000000000000..61f3787c113ed773a842feb9505ce2834f8a56c7 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated/models/Payment.ts @@ -0,0 +1,278 @@ +import { CheckException, EntityKey, EntityRoot, GraphMutationSession, LoadedScalarSnapshot, MutationIntent, MutationTraceScope, ObjectLocation, SmartList, TeaQLDataService, UserContext } from '../../teaql-ts'; +import { CustomerOrder } from './CustomerOrder'; +import { PaymentAttempt } from './PaymentAttempt'; + +export class Payment { + private static teaqlTemporaryId = 0; + id?: string; + customerOrder?: CustomerOrder | string; + referenceCode?: string; + version?: number; + + constructor(init?: Partial) { + Object.defineProperty(this, "_action", { value: init && init.id ? "Update" : "Create", writable: true, enumerable: false }); + Object.defineProperty(this, "_comment", { value: undefined, writable: true, enumerable: false }); + Object.defineProperty(this, "_fullyLoaded", { value: !init || !init.id, writable: true, enumerable: false }); + Object.defineProperty(this, "_loadedFields", { value: new Set(Object.keys(init || {})), writable: true, enumerable: false }); + Object.defineProperty(this, "_root", { value: new EntityRoot(), writable: true, enumerable: false }); + Object.defineProperty(this, "_ledgerId", { value: init?.id ?? --Payment.teaqlTemporaryId, writable: true, enumerable: false }); + Object.defineProperty(this, "_paymentAttemptList", { value: init?.id ? SmartList.empty() : new SmartList(), writable: true, enumerable: false }); + if (init) { + const fields: Record = { ...(init as Record) }; + delete fields["paymentAttemptList"]; + Object.assign(this, fields); + if ((init as any).customerOrder && typeof (init as any).customerOrder === "object") { + this.customerOrder = (init as any).customerOrder instanceof CustomerOrder + ? (init as any).customerOrder + : CustomerOrder.fromRecord((init as any).customerOrder); + this.markLoaded("customerOrder"); + } + if (Array.isArray((init as any).paymentAttemptList)) { + const selected = (init as any).paymentAttemptList; + (this as any)._paymentAttemptList = new SmartList(selected.map( + (child: unknown) => child instanceof PaymentAttempt + ? child + : PaymentAttempt.fromRecord(child as Record), + ), { totalCount: selected.totalCount, aggregations: selected.aggregations, + summary: selected.summary, facets: selected.facets, isLoaded: selected.isLoaded ?? true }); + this.markLoaded("paymentAttemptList"); + } + } + const key = this.teaqlEntityKey(); + if ((this as any)._action === "Create") (this as any)._root.markAsNew(key); + else if ((this as any).version !== undefined) (this as any)._root.setOriginalVersion(key, Number((this as any).version)); + LoadedScalarSnapshot.captureFor(this, this.teaqlMutationPayload()); + } + + private teaqlEntityKey(): EntityKey { return { entity: "Payment", id: (this as any)._ledgerId }; } + private teaqlAttachRoot(root: EntityRoot): this { + if ((this as any)._root !== root) { + root.mergeEntityFrom((this as any)._root, this.teaqlEntityKey()); + (this as any)._root = root; + } + for (const child of (this as any)._paymentAttemptList) child.teaqlAttachRoot(root); + return this; + } + + static fromRecord(record: Record, root?: EntityRoot): Payment { + const entity = new Payment(record as Partial); + return root ? entity.teaqlAttachRoot(root) : entity; + } + + isLoaded(field: string): boolean { + return (this as any)._fullyLoaded || (this as any)._loadedFields.has(field); + } + + markLoaded(...fields: string[]): this { + for (const field of fields) (this as any)._loadedFields.add(field); + return this; + } + + markLoadedOnly(...fields: string[]): this { + (this as any)._fullyLoaded = false; + (this as any)._loadedFields = new Set(fields); + return this; + } + + static refer(id: string | number): Payment { + return new Payment({ id: String(id) } as Partial); + } + + /** @internal Generated Runtime Module fixed-ID construction capability. */ + static teaqlBootstrapNew(id: string | number): Payment { + return new Payment().updateId(String(id)); + } + + markForDeletion(): this { + (this as any)._action = "Delete"; + (this as any)._root.markAsDeleted(this.teaqlEntityKey()); + return this; + } + + auditAs(comment: string): this { + new MutationIntent(comment); + (this as any)._comment = comment; + return this; + } + + async save(context: UserContext): Promise { + const service = context.requireResource("dataService"); + return service.executeGraphSave(new MutationIntent((this as any)._comment), async (graph) => { + this.teaqlPreflightGraph(context, service, graph); + return this.teaqlSaveWithinGraph(context, service, graph); + }); + } + + /** @internal Validates and fixes the complete graph before its first mutation. */ + teaqlPreflightGraph(context: UserContext, service: TeaQLDataService, graph: GraphMutationSession, parent?: MutationTraceScope): void { + if (!parent) new MutationIntent((this as any)._comment); + const action = (this as any)._action; + const pending = action !== "Update" || (this as any)._root.hasPending(this.teaqlEntityKey()); + if (action === "Update" && pending) { + const notLoaded = [{ member: "id", canonical: "id" }, { member: "customerOrder", canonical: "customer_order" }, { member: "referenceCode", canonical: "reference_code" }, { member: "version", canonical: "version" }] + .find(field => !this.isLoaded(field.member)); + if (notLoaded) { + throw new CheckException([{ + ruleId: "invalid_type", + location: ObjectLocation.property(notLoaded.canonical), + message: "Mutation requires a fully loaded entity", + }]); + } + } + const request = graph.request({ + entity: "Payment", action, + payload: action === "Update" + ? (this as any)._root.change(this.teaqlEntityKey()) + : this.teaqlMutationPayload(), + id: (this as any).id, version: (this as any).version, + comment: (this as any)._comment, + ledgerKey: this.teaqlEntityKey(), ledgerRoot: (this as any)._root, + }, parent, (this as any)._comment).withLoadedSnapshot(LoadedScalarSnapshot.forEntity(this) ?? new LoadedScalarSnapshot()); + if (pending) service.preflightMutation(request); + const scope = request.scopeFor(this.teaqlEntityKey()); + for (const [index, child] of (this as any)._paymentAttemptList.entries()) { + child.teaqlAttachRoot((this as any)._root); + const currentParent = child.payment; + if (this.id === undefined || !child.isLoaded("payment") || (typeof currentParent === "object" && currentParent !== null + ? currentParent.id : currentParent) !== this.id) child.updatePayment(this); + try { child.teaqlPreflightGraph(context, service, graph, scope); } + catch (error) { + if (!(error instanceof CheckException)) throw error; + const prefix = ObjectLocation.property("payment_attempt_list").index(index); + throw new CheckException(error.violations.map(violation => ({ + ...violation, location: violation.location.prefixedBy(prefix), + }))); + } + } + } + + /** @internal Used by generated relation cascades inside the root graph transaction. */ + async teaqlSaveWithinGraph(context: UserContext, service: TeaQLDataService, graph: GraphMutationSession, parent?: MutationTraceScope): Promise { + if (!parent) new MutationIntent((this as any)._comment); + const action = (this as any)._action; + const ledgerPayload = (this as any)._root.change(this.teaqlEntityKey()); + const mutation = { + entity: "Payment", + action: (this as any)._action, + payload: action === "Update" ? ledgerPayload : this.teaqlMutationPayload(), + id: (this as any).id, + version: (this as any).version, + comment: (this as any)._comment + ,ledgerKey: this.teaqlEntityKey() + ,ledgerRoot: (this as any)._root + }; + const request = graph.request(mutation, parent, (this as any)._comment) + .withLoadedSnapshot(LoadedScalarSnapshot.forEntity(this) ?? new LoadedScalarSnapshot()); + if (action === "Update" && !(this as any)._root.hasPending(this.teaqlEntityKey())) { + const scope = request.scopeFor(this.teaqlEntityKey()); + for (const [index, child] of (this as any)._paymentAttemptList.entries()) { + child.teaqlAttachRoot((this as any)._root); + const currentParent = child.payment; + if (this.id === undefined || !child.isLoaded("payment") || (typeof currentParent === "object" && currentParent !== null + ? currentParent.id : currentParent) !== this.id) child.updatePayment(this); + try { await child.teaqlSaveWithinGraph(context, service, graph, scope); } + catch (error) { + if (!(error instanceof CheckException)) throw error; + const prefix = ObjectLocation.property("payment_attempt_list").index(index); + throw new CheckException(error.violations.map(violation => ({ + ...violation, location: violation.location.prefixedBy(prefix), + }))); + } + } + return this; + } + const result = await service.executeMutation(request); + for (const [field, value] of Object.entries(mutation.payload as Record)) { + if (field !== "id" && field !== "version") (this as any)._root.set(this.teaqlEntityKey(), field, value); + } + if (!result.persistedRecord) { + throw new Error("Mutation did not return the authoritative persisted record"); + } + const rollbackState = { + payload: this.teaqlMutationPayload(), + ledgerId: (this as any)._ledgerId, + action: (this as any)._action, + loadedFields: new Set((this as any)._loadedFields), + fullyLoaded: (this as any)._fullyLoaded, + loadedSnapshot: LoadedScalarSnapshot.forEntity(this), + }; + const oldKey = this.teaqlEntityKey(); + Object.assign(this, result.persistedRecord); + LoadedScalarSnapshot.captureFor(this, this.teaqlMutationPayload()); + (this as any)._ledgerId = (this as any).id ?? (this as any)._ledgerId; + const newKey = this.teaqlEntityKey(); + (this as any)._root.rekey(oldKey, newKey); + service.afterGraphRollback(() => { + Object.assign(this, rollbackState.payload); + (this as any)._ledgerId = rollbackState.ledgerId; + (this as any)._action = rollbackState.action; + (this as any)._loadedFields = rollbackState.loadedFields; + (this as any)._fullyLoaded = rollbackState.fullyLoaded; + LoadedScalarSnapshot.restoreFor(this, rollbackState.loadedSnapshot); + (this as any)._root.rekey(newKey, oldKey); + }); + (this as any)._loadedFields = new Set(Object.keys(result.persistedRecord)); + (this as any)._fullyLoaded = false; + if (mutation.action !== "Delete") (this as any)._action = "Update"; + const scope = request.scopeFor(newKey); + for (const [index, child] of (this as any)._paymentAttemptList.entries()) { + child.teaqlAttachRoot((this as any)._root); + const currentParent = child.payment; + if (this.id === undefined || !child.isLoaded("payment") || (typeof currentParent === "object" && currentParent !== null + ? currentParent.id : currentParent) !== this.id) child.updatePayment(this); + try { await child.teaqlSaveWithinGraph(context, service, graph, scope); } + catch (error) { + if (!(error instanceof CheckException)) throw error; + const prefix = ObjectLocation.property("payment_attempt_list").index(index); + throw new CheckException(error.violations.map(violation => ({ + ...violation, location: violation.location.prefixedBy(prefix), + }))); + } + } + service.afterGraphCommit(() => { + (this as any)._root.clearEntity(newKey); + if ((this as any).version !== undefined) (this as any)._root.acceptCommittedVersion(newKey, Number((this as any).version)); + }); + return this; + } + + private teaqlMutationPayload(): Record { + return { + "id": this.id, + "customer_order": this.customerOrder, + "reference_code": this.referenceCode, + "version": this.version + }; + } + + updateId(value: string): this { + this.id = value; + this.markLoaded("id"); + (this as any)._root.set(this.teaqlEntityKey(), "id", value); + return this; + } + + updateReferenceCode(value: string): this { + this.referenceCode = value; + this.markLoaded("referenceCode"); + (this as any)._root.set(this.teaqlEntityKey(), "reference_code", value); + return this; + } + + updateVersion(value: number): this { + this.version = value; + this.markLoaded("version"); + (this as any)._root.set(this.teaqlEntityKey(), "version", value); + return this; + } + updateCustomerOrder(value: any): this { + this.customerOrder = value?.id ?? value; + this.markLoaded("customerOrder"); + (this as any)._root.set(this.teaqlEntityKey(), "customer_order", this.customerOrder); + return this; + } + + paymentAttemptList(): SmartList { + return (this as any)._paymentAttemptList; + } +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated/models/PaymentAttempt.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated/models/PaymentAttempt.ts new file mode 100644 index 0000000000000000000000000000000000000000..141650904f4f728ea4529ff9c2128baa08b9b8e1 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated/models/PaymentAttempt.ts @@ -0,0 +1,219 @@ +import { CheckException, EntityKey, EntityRoot, GraphMutationSession, LoadedScalarSnapshot, MutationIntent, MutationTraceScope, ObjectLocation, SmartList, TeaQLDataService, UserContext } from '../../teaql-ts'; +import { Payment } from './Payment'; + +export class PaymentAttempt { + private static teaqlTemporaryId = 0; + id?: string; + payment?: Payment | string; + referenceCode?: string; + version?: number; + + constructor(init?: Partial) { + Object.defineProperty(this, "_action", { value: init && init.id ? "Update" : "Create", writable: true, enumerable: false }); + Object.defineProperty(this, "_comment", { value: undefined, writable: true, enumerable: false }); + Object.defineProperty(this, "_fullyLoaded", { value: !init || !init.id, writable: true, enumerable: false }); + Object.defineProperty(this, "_loadedFields", { value: new Set(Object.keys(init || {})), writable: true, enumerable: false }); + Object.defineProperty(this, "_root", { value: new EntityRoot(), writable: true, enumerable: false }); + Object.defineProperty(this, "_ledgerId", { value: init?.id ?? --PaymentAttempt.teaqlTemporaryId, writable: true, enumerable: false }); + if (init) { + const fields: Record = { ...(init as Record) }; + Object.assign(this, fields); + if ((init as any).payment && typeof (init as any).payment === "object") { + this.payment = (init as any).payment instanceof Payment + ? (init as any).payment + : Payment.fromRecord((init as any).payment); + this.markLoaded("payment"); + } + } + const key = this.teaqlEntityKey(); + if ((this as any)._action === "Create") (this as any)._root.markAsNew(key); + else if ((this as any).version !== undefined) (this as any)._root.setOriginalVersion(key, Number((this as any).version)); + LoadedScalarSnapshot.captureFor(this, this.teaqlMutationPayload()); + } + + private teaqlEntityKey(): EntityKey { return { entity: "PaymentAttempt", id: (this as any)._ledgerId }; } + private teaqlAttachRoot(root: EntityRoot): this { + if ((this as any)._root !== root) { + root.mergeEntityFrom((this as any)._root, this.teaqlEntityKey()); + (this as any)._root = root; + } + return this; + } + + static fromRecord(record: Record, root?: EntityRoot): PaymentAttempt { + const entity = new PaymentAttempt(record as Partial); + return root ? entity.teaqlAttachRoot(root) : entity; + } + + isLoaded(field: string): boolean { + return (this as any)._fullyLoaded || (this as any)._loadedFields.has(field); + } + + markLoaded(...fields: string[]): this { + for (const field of fields) (this as any)._loadedFields.add(field); + return this; + } + + markLoadedOnly(...fields: string[]): this { + (this as any)._fullyLoaded = false; + (this as any)._loadedFields = new Set(fields); + return this; + } + + static refer(id: string | number): PaymentAttempt { + return new PaymentAttempt({ id: String(id) } as Partial); + } + + /** @internal Generated Runtime Module fixed-ID construction capability. */ + static teaqlBootstrapNew(id: string | number): PaymentAttempt { + return new PaymentAttempt().updateId(String(id)); + } + + markForDeletion(): this { + (this as any)._action = "Delete"; + (this as any)._root.markAsDeleted(this.teaqlEntityKey()); + return this; + } + + auditAs(comment: string): this { + new MutationIntent(comment); + (this as any)._comment = comment; + return this; + } + + async save(context: UserContext): Promise { + const service = context.requireResource("dataService"); + return service.executeGraphSave(new MutationIntent((this as any)._comment), async (graph) => { + this.teaqlPreflightGraph(context, service, graph); + return this.teaqlSaveWithinGraph(context, service, graph); + }); + } + + /** @internal Validates and fixes the complete graph before its first mutation. */ + teaqlPreflightGraph(context: UserContext, service: TeaQLDataService, graph: GraphMutationSession, parent?: MutationTraceScope): void { + if (!parent) new MutationIntent((this as any)._comment); + const action = (this as any)._action; + const pending = action !== "Update" || (this as any)._root.hasPending(this.teaqlEntityKey()); + if (action === "Update" && pending) { + const notLoaded = [{ member: "id", canonical: "id" }, { member: "payment", canonical: "payment" }, { member: "referenceCode", canonical: "reference_code" }, { member: "version", canonical: "version" }] + .find(field => !this.isLoaded(field.member)); + if (notLoaded) { + throw new CheckException([{ + ruleId: "invalid_type", + location: ObjectLocation.property(notLoaded.canonical), + message: "Mutation requires a fully loaded entity", + }]); + } + } + const request = graph.request({ + entity: "PaymentAttempt", action, + payload: action === "Update" + ? (this as any)._root.change(this.teaqlEntityKey()) + : this.teaqlMutationPayload(), + id: (this as any).id, version: (this as any).version, + comment: (this as any)._comment, + ledgerKey: this.teaqlEntityKey(), ledgerRoot: (this as any)._root, + }, parent, (this as any)._comment).withLoadedSnapshot(LoadedScalarSnapshot.forEntity(this) ?? new LoadedScalarSnapshot()); + if (pending) service.preflightMutation(request); + const scope = request.scopeFor(this.teaqlEntityKey()); + } + + /** @internal Used by generated relation cascades inside the root graph transaction. */ + async teaqlSaveWithinGraph(context: UserContext, service: TeaQLDataService, graph: GraphMutationSession, parent?: MutationTraceScope): Promise { + if (!parent) new MutationIntent((this as any)._comment); + const action = (this as any)._action; + const ledgerPayload = (this as any)._root.change(this.teaqlEntityKey()); + const mutation = { + entity: "PaymentAttempt", + action: (this as any)._action, + payload: action === "Update" ? ledgerPayload : this.teaqlMutationPayload(), + id: (this as any).id, + version: (this as any).version, + comment: (this as any)._comment + ,ledgerKey: this.teaqlEntityKey() + ,ledgerRoot: (this as any)._root + }; + const request = graph.request(mutation, parent, (this as any)._comment) + .withLoadedSnapshot(LoadedScalarSnapshot.forEntity(this) ?? new LoadedScalarSnapshot()); + if (action === "Update" && !(this as any)._root.hasPending(this.teaqlEntityKey())) { + const scope = request.scopeFor(this.teaqlEntityKey()); + return this; + } + const result = await service.executeMutation(request); + for (const [field, value] of Object.entries(mutation.payload as Record)) { + if (field !== "id" && field !== "version") (this as any)._root.set(this.teaqlEntityKey(), field, value); + } + if (!result.persistedRecord) { + throw new Error("Mutation did not return the authoritative persisted record"); + } + const rollbackState = { + payload: this.teaqlMutationPayload(), + ledgerId: (this as any)._ledgerId, + action: (this as any)._action, + loadedFields: new Set((this as any)._loadedFields), + fullyLoaded: (this as any)._fullyLoaded, + loadedSnapshot: LoadedScalarSnapshot.forEntity(this), + }; + const oldKey = this.teaqlEntityKey(); + Object.assign(this, result.persistedRecord); + LoadedScalarSnapshot.captureFor(this, this.teaqlMutationPayload()); + (this as any)._ledgerId = (this as any).id ?? (this as any)._ledgerId; + const newKey = this.teaqlEntityKey(); + (this as any)._root.rekey(oldKey, newKey); + service.afterGraphRollback(() => { + Object.assign(this, rollbackState.payload); + (this as any)._ledgerId = rollbackState.ledgerId; + (this as any)._action = rollbackState.action; + (this as any)._loadedFields = rollbackState.loadedFields; + (this as any)._fullyLoaded = rollbackState.fullyLoaded; + LoadedScalarSnapshot.restoreFor(this, rollbackState.loadedSnapshot); + (this as any)._root.rekey(newKey, oldKey); + }); + (this as any)._loadedFields = new Set(Object.keys(result.persistedRecord)); + (this as any)._fullyLoaded = false; + if (mutation.action !== "Delete") (this as any)._action = "Update"; + const scope = request.scopeFor(newKey); + service.afterGraphCommit(() => { + (this as any)._root.clearEntity(newKey); + if ((this as any).version !== undefined) (this as any)._root.acceptCommittedVersion(newKey, Number((this as any).version)); + }); + return this; + } + + private teaqlMutationPayload(): Record { + return { + "id": this.id, + "payment": this.payment, + "reference_code": this.referenceCode, + "version": this.version + }; + } + + updateId(value: string): this { + this.id = value; + this.markLoaded("id"); + (this as any)._root.set(this.teaqlEntityKey(), "id", value); + return this; + } + + updateReferenceCode(value: string): this { + this.referenceCode = value; + this.markLoaded("referenceCode"); + (this as any)._root.set(this.teaqlEntityKey(), "reference_code", value); + return this; + } + + updateVersion(value: number): this { + this.version = value; + this.markLoaded("version"); + (this as any)._root.set(this.teaqlEntityKey(), "version", value); + return this; + } + updatePayment(value: any): this { + this.payment = value?.id ?? value; + this.markLoaded("payment"); + (this as any)._root.set(this.teaqlEntityKey(), "payment", this.payment); + return this; + } + +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated/models/Platform.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated/models/Platform.ts new file mode 100644 index 0000000000000000000000000000000000000000..0561320937e12ecec16df9008b6ac5f1c29f6b51 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated/models/Platform.ts @@ -0,0 +1,262 @@ +import { CheckException, EntityKey, EntityRoot, GraphMutationSession, LoadedScalarSnapshot, MutationIntent, MutationTraceScope, ObjectLocation, SmartList, TeaQLDataService, UserContext } from '../../teaql-ts'; +import { CustomerOrder } from './CustomerOrder'; + +export class Platform { + private static teaqlTemporaryId = 0; + id?: string; + name?: string; + version?: number; + + constructor(init?: Partial) { + Object.defineProperty(this, "_action", { value: init && init.id ? "Update" : "Create", writable: true, enumerable: false }); + Object.defineProperty(this, "_comment", { value: undefined, writable: true, enumerable: false }); + Object.defineProperty(this, "_fullyLoaded", { value: !init || !init.id, writable: true, enumerable: false }); + Object.defineProperty(this, "_loadedFields", { value: new Set(Object.keys(init || {})), writable: true, enumerable: false }); + Object.defineProperty(this, "_root", { value: new EntityRoot(), writable: true, enumerable: false }); + Object.defineProperty(this, "_ledgerId", { value: init?.id ?? --Platform.teaqlTemporaryId, writable: true, enumerable: false }); + Object.defineProperty(this, "_customerOrderList", { value: init?.id ? SmartList.empty() : new SmartList(), writable: true, enumerable: false }); + if (init) { + const fields: Record = { ...(init as Record) }; + delete fields["customerOrderList"]; + Object.assign(this, fields); + if (Array.isArray((init as any).customerOrderList)) { + const selected = (init as any).customerOrderList; + (this as any)._customerOrderList = new SmartList(selected.map( + (child: unknown) => child instanceof CustomerOrder + ? child + : CustomerOrder.fromRecord(child as Record), + ), { totalCount: selected.totalCount, aggregations: selected.aggregations, + summary: selected.summary, facets: selected.facets, isLoaded: selected.isLoaded ?? true }); + this.markLoaded("customerOrderList"); + } + } + const key = this.teaqlEntityKey(); + if ((this as any)._action === "Create") (this as any)._root.markAsNew(key); + else if ((this as any).version !== undefined) (this as any)._root.setOriginalVersion(key, Number((this as any).version)); + LoadedScalarSnapshot.captureFor(this, this.teaqlMutationPayload()); + } + + private teaqlEntityKey(): EntityKey { return { entity: "Platform", id: (this as any)._ledgerId }; } + private teaqlAttachRoot(root: EntityRoot): this { + if ((this as any)._root !== root) { + root.mergeEntityFrom((this as any)._root, this.teaqlEntityKey()); + (this as any)._root = root; + } + for (const child of (this as any)._customerOrderList) child.teaqlAttachRoot(root); + return this; + } + + static fromRecord(record: Record, root?: EntityRoot): Platform { + const entity = new Platform(record as Partial); + return root ? entity.teaqlAttachRoot(root) : entity; + } + + isLoaded(field: string): boolean { + return (this as any)._fullyLoaded || (this as any)._loadedFields.has(field); + } + + markLoaded(...fields: string[]): this { + for (const field of fields) (this as any)._loadedFields.add(field); + return this; + } + + markLoadedOnly(...fields: string[]): this { + (this as any)._fullyLoaded = false; + (this as any)._loadedFields = new Set(fields); + return this; + } + + static refer(id: string | number): Platform { + return new Platform({ id: String(id) } as Partial); + } + + /** @internal Generated Runtime Module fixed-ID construction capability. */ + static teaqlBootstrapNew(id: string | number): Platform { + return new Platform().updateId(String(id)); + } + + markForDeletion(): this { + (this as any)._action = "Delete"; + (this as any)._root.markAsDeleted(this.teaqlEntityKey()); + return this; + } + + auditAs(comment: string): this { + new MutationIntent(comment); + (this as any)._comment = comment; + return this; + } + + async save(context: UserContext): Promise { + const service = context.requireResource("dataService"); + return service.executeGraphSave(new MutationIntent((this as any)._comment), async (graph) => { + this.teaqlPreflightGraph(context, service, graph); + return this.teaqlSaveWithinGraph(context, service, graph); + }); + } + + /** @internal Validates and fixes the complete graph before its first mutation. */ + teaqlPreflightGraph(context: UserContext, service: TeaQLDataService, graph: GraphMutationSession, parent?: MutationTraceScope): void { + if (!parent) new MutationIntent((this as any)._comment); + const action = (this as any)._action; + const pending = action !== "Update" || (this as any)._root.hasPending(this.teaqlEntityKey()); + if (action === "Update" && pending) { + const notLoaded = [{ member: "id", canonical: "id" }, { member: "name", canonical: "name" }, { member: "version", canonical: "version" }] + .find(field => !this.isLoaded(field.member)); + if (notLoaded) { + throw new CheckException([{ + ruleId: "invalid_type", + location: ObjectLocation.property(notLoaded.canonical), + message: "Mutation requires a fully loaded entity", + }]); + } + } + const request = graph.request({ + entity: "Platform", action, + payload: action === "Update" + ? (this as any)._root.change(this.teaqlEntityKey()) + : this.teaqlMutationPayload(), + id: (this as any).id, version: (this as any).version, + comment: (this as any)._comment, + ledgerKey: this.teaqlEntityKey(), ledgerRoot: (this as any)._root, + }, parent, (this as any)._comment).withLoadedSnapshot(LoadedScalarSnapshot.forEntity(this) ?? new LoadedScalarSnapshot()); + if (pending) service.preflightMutation(request); + const scope = request.scopeFor(this.teaqlEntityKey()); + for (const [index, child] of (this as any)._customerOrderList.entries()) { + child.teaqlAttachRoot((this as any)._root); + const currentParent = child.platform; + if (this.id === undefined || !child.isLoaded("platform") || (typeof currentParent === "object" && currentParent !== null + ? currentParent.id : currentParent) !== this.id) child.updatePlatform(this); + try { child.teaqlPreflightGraph(context, service, graph, scope); } + catch (error) { + if (!(error instanceof CheckException)) throw error; + const prefix = ObjectLocation.property("customer_order_list").index(index); + throw new CheckException(error.violations.map(violation => ({ + ...violation, location: violation.location.prefixedBy(prefix), + }))); + } + } + } + + /** @internal Used by generated relation cascades inside the root graph transaction. */ + async teaqlSaveWithinGraph(context: UserContext, service: TeaQLDataService, graph: GraphMutationSession, parent?: MutationTraceScope): Promise { + if (!parent) new MutationIntent((this as any)._comment); + const action = (this as any)._action; + const ledgerPayload = (this as any)._root.change(this.teaqlEntityKey()); + const mutation = { + entity: "Platform", + action: (this as any)._action, + payload: action === "Update" ? ledgerPayload : this.teaqlMutationPayload(), + id: (this as any).id, + version: (this as any).version, + comment: (this as any)._comment + ,ledgerKey: this.teaqlEntityKey() + ,ledgerRoot: (this as any)._root + }; + const request = graph.request(mutation, parent, (this as any)._comment) + .withLoadedSnapshot(LoadedScalarSnapshot.forEntity(this) ?? new LoadedScalarSnapshot()); + if (action === "Update" && !(this as any)._root.hasPending(this.teaqlEntityKey())) { + const scope = request.scopeFor(this.teaqlEntityKey()); + for (const [index, child] of (this as any)._customerOrderList.entries()) { + child.teaqlAttachRoot((this as any)._root); + const currentParent = child.platform; + if (this.id === undefined || !child.isLoaded("platform") || (typeof currentParent === "object" && currentParent !== null + ? currentParent.id : currentParent) !== this.id) child.updatePlatform(this); + try { await child.teaqlSaveWithinGraph(context, service, graph, scope); } + catch (error) { + if (!(error instanceof CheckException)) throw error; + const prefix = ObjectLocation.property("customer_order_list").index(index); + throw new CheckException(error.violations.map(violation => ({ + ...violation, location: violation.location.prefixedBy(prefix), + }))); + } + } + return this; + } + const result = await service.executeMutation(request); + for (const [field, value] of Object.entries(mutation.payload as Record)) { + if (field !== "id" && field !== "version") (this as any)._root.set(this.teaqlEntityKey(), field, value); + } + if (!result.persistedRecord) { + throw new Error("Mutation did not return the authoritative persisted record"); + } + const rollbackState = { + payload: this.teaqlMutationPayload(), + ledgerId: (this as any)._ledgerId, + action: (this as any)._action, + loadedFields: new Set((this as any)._loadedFields), + fullyLoaded: (this as any)._fullyLoaded, + loadedSnapshot: LoadedScalarSnapshot.forEntity(this), + }; + const oldKey = this.teaqlEntityKey(); + Object.assign(this, result.persistedRecord); + LoadedScalarSnapshot.captureFor(this, this.teaqlMutationPayload()); + (this as any)._ledgerId = (this as any).id ?? (this as any)._ledgerId; + const newKey = this.teaqlEntityKey(); + (this as any)._root.rekey(oldKey, newKey); + service.afterGraphRollback(() => { + Object.assign(this, rollbackState.payload); + (this as any)._ledgerId = rollbackState.ledgerId; + (this as any)._action = rollbackState.action; + (this as any)._loadedFields = rollbackState.loadedFields; + (this as any)._fullyLoaded = rollbackState.fullyLoaded; + LoadedScalarSnapshot.restoreFor(this, rollbackState.loadedSnapshot); + (this as any)._root.rekey(newKey, oldKey); + }); + (this as any)._loadedFields = new Set(Object.keys(result.persistedRecord)); + (this as any)._fullyLoaded = false; + if (mutation.action !== "Delete") (this as any)._action = "Update"; + const scope = request.scopeFor(newKey); + for (const [index, child] of (this as any)._customerOrderList.entries()) { + child.teaqlAttachRoot((this as any)._root); + const currentParent = child.platform; + if (this.id === undefined || !child.isLoaded("platform") || (typeof currentParent === "object" && currentParent !== null + ? currentParent.id : currentParent) !== this.id) child.updatePlatform(this); + try { await child.teaqlSaveWithinGraph(context, service, graph, scope); } + catch (error) { + if (!(error instanceof CheckException)) throw error; + const prefix = ObjectLocation.property("customer_order_list").index(index); + throw new CheckException(error.violations.map(violation => ({ + ...violation, location: violation.location.prefixedBy(prefix), + }))); + } + } + service.afterGraphCommit(() => { + (this as any)._root.clearEntity(newKey); + if ((this as any).version !== undefined) (this as any)._root.acceptCommittedVersion(newKey, Number((this as any).version)); + }); + return this; + } + + private teaqlMutationPayload(): Record { + return { + "id": this.id, + "name": this.name, + "version": this.version + }; + } + + updateId(value: string): this { + this.id = value; + this.markLoaded("id"); + (this as any)._root.set(this.teaqlEntityKey(), "id", value); + return this; + } + + updateName(value: string): this { + this.name = value; + this.markLoaded("name"); + (this as any)._root.set(this.teaqlEntityKey(), "name", value); + return this; + } + + updateVersion(value: number): this { + this.version = value; + this.markLoaded("version"); + (this as any)._root.set(this.teaqlEntityKey(), "version", value); + return this; + } + customerOrderList(): SmartList { + return (this as any)._customerOrderList; + } +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated/models/Shipment.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated/models/Shipment.ts new file mode 100644 index 0000000000000000000000000000000000000000..a5f6d18f4b12c44151ee4ec630c4a91702043d6e --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated/models/Shipment.ts @@ -0,0 +1,219 @@ +import { CheckException, EntityKey, EntityRoot, GraphMutationSession, LoadedScalarSnapshot, MutationIntent, MutationTraceScope, ObjectLocation, SmartList, TeaQLDataService, UserContext } from '../../teaql-ts'; +import { CustomerOrder } from './CustomerOrder'; + +export class Shipment { + private static teaqlTemporaryId = 0; + id?: string; + customerOrder?: CustomerOrder | string; + referenceCode?: string; + version?: number; + + constructor(init?: Partial) { + Object.defineProperty(this, "_action", { value: init && init.id ? "Update" : "Create", writable: true, enumerable: false }); + Object.defineProperty(this, "_comment", { value: undefined, writable: true, enumerable: false }); + Object.defineProperty(this, "_fullyLoaded", { value: !init || !init.id, writable: true, enumerable: false }); + Object.defineProperty(this, "_loadedFields", { value: new Set(Object.keys(init || {})), writable: true, enumerable: false }); + Object.defineProperty(this, "_root", { value: new EntityRoot(), writable: true, enumerable: false }); + Object.defineProperty(this, "_ledgerId", { value: init?.id ?? --Shipment.teaqlTemporaryId, writable: true, enumerable: false }); + if (init) { + const fields: Record = { ...(init as Record) }; + Object.assign(this, fields); + if ((init as any).customerOrder && typeof (init as any).customerOrder === "object") { + this.customerOrder = (init as any).customerOrder instanceof CustomerOrder + ? (init as any).customerOrder + : CustomerOrder.fromRecord((init as any).customerOrder); + this.markLoaded("customerOrder"); + } + } + const key = this.teaqlEntityKey(); + if ((this as any)._action === "Create") (this as any)._root.markAsNew(key); + else if ((this as any).version !== undefined) (this as any)._root.setOriginalVersion(key, Number((this as any).version)); + LoadedScalarSnapshot.captureFor(this, this.teaqlMutationPayload()); + } + + private teaqlEntityKey(): EntityKey { return { entity: "Shipment", id: (this as any)._ledgerId }; } + private teaqlAttachRoot(root: EntityRoot): this { + if ((this as any)._root !== root) { + root.mergeEntityFrom((this as any)._root, this.teaqlEntityKey()); + (this as any)._root = root; + } + return this; + } + + static fromRecord(record: Record, root?: EntityRoot): Shipment { + const entity = new Shipment(record as Partial); + return root ? entity.teaqlAttachRoot(root) : entity; + } + + isLoaded(field: string): boolean { + return (this as any)._fullyLoaded || (this as any)._loadedFields.has(field); + } + + markLoaded(...fields: string[]): this { + for (const field of fields) (this as any)._loadedFields.add(field); + return this; + } + + markLoadedOnly(...fields: string[]): this { + (this as any)._fullyLoaded = false; + (this as any)._loadedFields = new Set(fields); + return this; + } + + static refer(id: string | number): Shipment { + return new Shipment({ id: String(id) } as Partial); + } + + /** @internal Generated Runtime Module fixed-ID construction capability. */ + static teaqlBootstrapNew(id: string | number): Shipment { + return new Shipment().updateId(String(id)); + } + + markForDeletion(): this { + (this as any)._action = "Delete"; + (this as any)._root.markAsDeleted(this.teaqlEntityKey()); + return this; + } + + auditAs(comment: string): this { + new MutationIntent(comment); + (this as any)._comment = comment; + return this; + } + + async save(context: UserContext): Promise { + const service = context.requireResource("dataService"); + return service.executeGraphSave(new MutationIntent((this as any)._comment), async (graph) => { + this.teaqlPreflightGraph(context, service, graph); + return this.teaqlSaveWithinGraph(context, service, graph); + }); + } + + /** @internal Validates and fixes the complete graph before its first mutation. */ + teaqlPreflightGraph(context: UserContext, service: TeaQLDataService, graph: GraphMutationSession, parent?: MutationTraceScope): void { + if (!parent) new MutationIntent((this as any)._comment); + const action = (this as any)._action; + const pending = action !== "Update" || (this as any)._root.hasPending(this.teaqlEntityKey()); + if (action === "Update" && pending) { + const notLoaded = [{ member: "id", canonical: "id" }, { member: "customerOrder", canonical: "customer_order" }, { member: "referenceCode", canonical: "reference_code" }, { member: "version", canonical: "version" }] + .find(field => !this.isLoaded(field.member)); + if (notLoaded) { + throw new CheckException([{ + ruleId: "invalid_type", + location: ObjectLocation.property(notLoaded.canonical), + message: "Mutation requires a fully loaded entity", + }]); + } + } + const request = graph.request({ + entity: "Shipment", action, + payload: action === "Update" + ? (this as any)._root.change(this.teaqlEntityKey()) + : this.teaqlMutationPayload(), + id: (this as any).id, version: (this as any).version, + comment: (this as any)._comment, + ledgerKey: this.teaqlEntityKey(), ledgerRoot: (this as any)._root, + }, parent, (this as any)._comment).withLoadedSnapshot(LoadedScalarSnapshot.forEntity(this) ?? new LoadedScalarSnapshot()); + if (pending) service.preflightMutation(request); + const scope = request.scopeFor(this.teaqlEntityKey()); + } + + /** @internal Used by generated relation cascades inside the root graph transaction. */ + async teaqlSaveWithinGraph(context: UserContext, service: TeaQLDataService, graph: GraphMutationSession, parent?: MutationTraceScope): Promise { + if (!parent) new MutationIntent((this as any)._comment); + const action = (this as any)._action; + const ledgerPayload = (this as any)._root.change(this.teaqlEntityKey()); + const mutation = { + entity: "Shipment", + action: (this as any)._action, + payload: action === "Update" ? ledgerPayload : this.teaqlMutationPayload(), + id: (this as any).id, + version: (this as any).version, + comment: (this as any)._comment + ,ledgerKey: this.teaqlEntityKey() + ,ledgerRoot: (this as any)._root + }; + const request = graph.request(mutation, parent, (this as any)._comment) + .withLoadedSnapshot(LoadedScalarSnapshot.forEntity(this) ?? new LoadedScalarSnapshot()); + if (action === "Update" && !(this as any)._root.hasPending(this.teaqlEntityKey())) { + const scope = request.scopeFor(this.teaqlEntityKey()); + return this; + } + const result = await service.executeMutation(request); + for (const [field, value] of Object.entries(mutation.payload as Record)) { + if (field !== "id" && field !== "version") (this as any)._root.set(this.teaqlEntityKey(), field, value); + } + if (!result.persistedRecord) { + throw new Error("Mutation did not return the authoritative persisted record"); + } + const rollbackState = { + payload: this.teaqlMutationPayload(), + ledgerId: (this as any)._ledgerId, + action: (this as any)._action, + loadedFields: new Set((this as any)._loadedFields), + fullyLoaded: (this as any)._fullyLoaded, + loadedSnapshot: LoadedScalarSnapshot.forEntity(this), + }; + const oldKey = this.teaqlEntityKey(); + Object.assign(this, result.persistedRecord); + LoadedScalarSnapshot.captureFor(this, this.teaqlMutationPayload()); + (this as any)._ledgerId = (this as any).id ?? (this as any)._ledgerId; + const newKey = this.teaqlEntityKey(); + (this as any)._root.rekey(oldKey, newKey); + service.afterGraphRollback(() => { + Object.assign(this, rollbackState.payload); + (this as any)._ledgerId = rollbackState.ledgerId; + (this as any)._action = rollbackState.action; + (this as any)._loadedFields = rollbackState.loadedFields; + (this as any)._fullyLoaded = rollbackState.fullyLoaded; + LoadedScalarSnapshot.restoreFor(this, rollbackState.loadedSnapshot); + (this as any)._root.rekey(newKey, oldKey); + }); + (this as any)._loadedFields = new Set(Object.keys(result.persistedRecord)); + (this as any)._fullyLoaded = false; + if (mutation.action !== "Delete") (this as any)._action = "Update"; + const scope = request.scopeFor(newKey); + service.afterGraphCommit(() => { + (this as any)._root.clearEntity(newKey); + if ((this as any).version !== undefined) (this as any)._root.acceptCommittedVersion(newKey, Number((this as any).version)); + }); + return this; + } + + private teaqlMutationPayload(): Record { + return { + "id": this.id, + "customer_order": this.customerOrder, + "reference_code": this.referenceCode, + "version": this.version + }; + } + + updateId(value: string): this { + this.id = value; + this.markLoaded("id"); + (this as any)._root.set(this.teaqlEntityKey(), "id", value); + return this; + } + + updateReferenceCode(value: string): this { + this.referenceCode = value; + this.markLoaded("referenceCode"); + (this as any)._root.set(this.teaqlEntityKey(), "reference_code", value); + return this; + } + + updateVersion(value: number): this { + this.version = value; + this.markLoaded("version"); + (this as any)._root.set(this.teaqlEntityKey(), "version", value); + return this; + } + updateCustomerOrder(value: any): this { + this.customerOrder = value?.id ?? value; + this.markLoaded("customerOrder"); + (this as any)._root.set(this.teaqlEntityKey(), "customer_order", this.customerOrder); + return this; + } + +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/CustomerOrderRequest.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/CustomerOrderRequest.ts new file mode 100644 index 0000000000000000000000000000000000000000..60dc6c51d41c84e7b7e5380c9e55dbe472f9cb7d --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/CustomerOrderRequest.ts @@ -0,0 +1,907 @@ +import { EntityRoot, SelectQuery, SmartList, TeaQLDataService, UserContext, executeRelationFacets } from '../../teaql-ts'; +import { CustomerOrder } from '../models/CustomerOrder'; +import { mergeGeneratedScalarSearch, DynamicSearchWarning, QueryIntent } from 'teaql-ts'; +import { GENERATED_SEARCH_MODELS } from '../../generated-search-models'; + + + +export class CustomerOrderRequest { + private query: SelectQuery; + private filters: any[] = []; + private _purpose: string | undefined; + private _comment: string | undefined; + private _pageCountAlias: string | undefined; + private _pageCountMethod: "count" | "countAs" | undefined; + private _dynamicSearchWarnings: DynamicSearchWarning[] = []; + + constructor(minimal = false) { + this.query = new SelectQuery("CustomerOrder"); + this.filters.push({ version: { "$gte": 1 } }); + if (minimal) { + this.selectId(); + this.selectVersion(); + } + else this.selectSelfFields(); + } + + comment(c: string): this { + this.query.comment(c); + this._comment = c; + return this; + } + + filterByJson(source: unknown, warn?: (warning: DynamicSearchWarning) => void): this { + const base = this.query.clone(); + if (this.filters.length > 0) base.filter({ "$and": this.filters }); + const merged = mergeGeneratedScalarSearch(base, source, GENERATED_SEARCH_MODELS, warn); + this.query = merged.query as SelectQuery; + this.filters = this.query.filterCondition ? [this.query.filterCondition] : []; + this._dynamicSearchWarnings.push(...merged.warnings); + return this; + } + + get dynamicSearchWarnings(): readonly DynamicSearchWarning[] { + return this._dynamicSearchWarnings.map(warning => ({ ...warning })); + } + + purpose(p: string): ExecutableCustomerOrderRequest { + this.query.purpose(p); + this._purpose = p; + return new ExecutableCustomerOrderRequest( + (context) => context.withQueryRequest(this.query, () => this.executeForListInternal(context)), + (context) => context.withQueryRequest(this.query, () => this.executeForRowsInternal(context)), + (context, chunkSize) => this.executeForStreamInternal(context, chunkSize), + () => this.limit(1), + (c) => this.comment(c), + () => this.ensureIntent()); + } + + optimizeForContinuousPageFetch(): this { + this.query.optimizeForContinuousPageFetch(); + return this; + } + + optimizeForContinuousPageFetchWith(namespace: string, ttlSeconds: number): this { + this.query.optimizeForContinuousPageFetchWith(namespace, ttlSeconds); + return this; + } + + optimizePaginationWithIdSet(): this { + this.query.optimizePaginationWithIdSet(); + return this; + } + + optimizePaginationWithIdSetConfig(namespace: string, ttlSeconds: number, maxIds: number): this { + this.query.optimizePaginationWithIdSetConfig(namespace, ttlSeconds, maxIds); + return this; + } + + topNProbeParentThreshold(threshold: number): this { + this.query.topNProbeParentThreshold(threshold); + return this; + } + + limit(n: number): this { + this.query.limit(n); + return this; + } + + offset(n: number): this { + this.query.offset(n); + return this; + } + + toQuery(): SelectQuery { + if (this.filters.length > 0) { + this.query.filter({ "$and": this.filters }); + } + return this.query; + } + + withDeletedRows(): this { + this.filters = this.filters.filter(filter => !("version" in filter)); + return this; + } + + deletedRowsOnly(): this { + this.withDeletedRows(); + this.filters.push({ version: { "$lte": -1 } }); + return this; + } + + selectSelfFields(): this { + this.query.select(["id", "platform", "orderNumber", "description", "version"]); + return this; + } + + selectId(): this { + this.query.select(["id"]); + return this; + } + + + selectOrderNumber(): this { + this.query.select(["orderNumber"]); + return this; + } + + selectDescription(): this { + this.query.select(["description"]); + return this; + } + + selectVersion(): this { + this.query.select(["version"]); + return this; + } + + + selectPlatformWith(request: { toQuery(): SelectQuery }): this { + this.query.select(["platform"]); + this.query.relationQuery( + "platform", request.toQuery(), "platform", "id", false, + { kind: "relation-select", method: "selectPlatformWith", relation: "platform" }); + return this; + } + + withPlatformMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "platform": { + "$inSubquery": { query: request.toQuery(), field: "id" }, + }, + }); + return this; + } + + withoutPlatformMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "platform": { + "$notInSubquery": { query: request.toQuery(), field: "id" }, + }, + }); + return this; + } + + selectOrderItemListWith(request: { toQuery(): SelectQuery }): this { + this.query.relationQuery( + "orderItemList", request.toQuery(), "id", "customerOrder", true, + { kind: "relation-select", method: "selectOrderItemListWith", relation: "orderItemList" }); + return this; + } + selectPaymentListWith(request: { toQuery(): SelectQuery }): this { + this.query.relationQuery( + "paymentList", request.toQuery(), "id", "customerOrder", true, + { kind: "relation-select", method: "selectPaymentListWith", relation: "paymentList" }); + return this; + } + selectShipmentListWith(request: { toQuery(): SelectQuery }): this { + this.query.relationQuery( + "shipmentList", request.toQuery(), "id", "customerOrder", true, + { kind: "relation-select", method: "selectShipmentListWith", relation: "shipmentList" }); + return this; + } + + haveOrderItems(): this { + return this.withOrderItemListMatching({ + toQuery: () => new SelectQuery("OrderItem"), + }); + } + + haveNoOrderItems(): this { + return this.withoutOrderItemListMatching({ + toQuery: () => new SelectQuery("OrderItem"), + }); + } + + withOrderItemListMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "id": { + "$inSubquery": { + query: request.toQuery(), field: "customerOrder", + }, + }, + }); + return this; + } + + withoutOrderItemListMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "id": { + "$notInSubquery": { + query: request.toQuery(), field: "customerOrder", + }, + }, + }); + return this; + } + havePayments(): this { + return this.withPaymentListMatching({ + toQuery: () => new SelectQuery("Payment"), + }); + } + + haveNoPayments(): this { + return this.withoutPaymentListMatching({ + toQuery: () => new SelectQuery("Payment"), + }); + } + + withPaymentListMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "id": { + "$inSubquery": { + query: request.toQuery(), field: "customerOrder", + }, + }, + }); + return this; + } + + withoutPaymentListMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "id": { + "$notInSubquery": { + query: request.toQuery(), field: "customerOrder", + }, + }, + }); + return this; + } + haveShipments(): this { + return this.withShipmentListMatching({ + toQuery: () => new SelectQuery("Shipment"), + }); + } + + haveNoShipments(): this { + return this.withoutShipmentListMatching({ + toQuery: () => new SelectQuery("Shipment"), + }); + } + + withShipmentListMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "id": { + "$inSubquery": { + query: request.toQuery(), field: "customerOrder", + }, + }, + }); + return this; + } + + withoutShipmentListMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "id": { + "$notInSubquery": { + query: request.toQuery(), field: "customerOrder", + }, + }, + }); + return this; + } + + countOrderItems(): this { + return this.countOrderItemsAs("countOrderItems"); + } + + countOrderItemsAs(alias: string): this { + const query = new SelectQuery("OrderItem").filter({ version: { "$gte": 1 } }); + this.countOrderItemsWith(alias, { toQuery: () => query }); + this.query.relationAggregates[this.query.relationAggregates.length - 1].operation = { + kind: "relation-count", method: "countOrderItemsAs", relation: "orderItemList", alias }; + return this; + } + + countOrderItemsWith(alias: string, child: { toQuery(): SelectQuery }): this { + const query = child.toQuery().clone(); + query.aggregateItems = []; + query.aggregate("Count", "id", alias); + this.query.relationAggregate("orderItemList", alias, query, true, + { kind: "relation-count", method: "countOrderItemsWith", relation: "orderItemList", alias }); + return this; + } + + + countPayments(): this { + return this.countPaymentsAs("countPayments"); + } + + countPaymentsAs(alias: string): this { + const query = new SelectQuery("Payment").filter({ version: { "$gte": 1 } }); + this.countPaymentsWith(alias, { toQuery: () => query }); + this.query.relationAggregates[this.query.relationAggregates.length - 1].operation = { + kind: "relation-count", method: "countPaymentsAs", relation: "paymentList", alias }; + return this; + } + + countPaymentsWith(alias: string, child: { toQuery(): SelectQuery }): this { + const query = child.toQuery().clone(); + query.aggregateItems = []; + query.aggregate("Count", "id", alias); + this.query.relationAggregate("paymentList", alias, query, true, + { kind: "relation-count", method: "countPaymentsWith", relation: "paymentList", alias }); + return this; + } + + + countShipments(): this { + return this.countShipmentsAs("countShipments"); + } + + countShipmentsAs(alias: string): this { + const query = new SelectQuery("Shipment").filter({ version: { "$gte": 1 } }); + this.countShipmentsWith(alias, { toQuery: () => query }); + this.query.relationAggregates[this.query.relationAggregates.length - 1].operation = { + kind: "relation-count", method: "countShipmentsAs", relation: "shipmentList", alias }; + return this; + } + + countShipmentsWith(alias: string, child: { toQuery(): SelectQuery }): this { + const query = child.toQuery().clone(); + query.aggregateItems = []; + query.aggregate("Count", "id", alias); + this.query.relationAggregate("shipmentList", alias, query, true, + { kind: "relation-count", method: "countShipmentsWith", relation: "shipmentList", alias }); + return this; + } + + + + withIdIs(val: any): this { + this.filters.push({ "id": { "$eq": val } }); + return this; + } + + withIdIsNot(val: any): this { + this.filters.push({ "id": { "$ne": val } }); + return this; + } + + withIdIn(...vals: any[]): this { + this.filters.push({ "id": { "$in": vals } }); + return this; + } + + withIdNotIn(...vals: any[]): this { + this.filters.push({ "id": { "$notIn": vals } }); + return this; + } + + withIdGreaterThan(val: any): this { + this.filters.push({ "id": { "$gt": val } }); + return this; + } + + withIdGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "id": { "$gte": val } }); + return this; + } + + withIdLessThan(val: any): this { + this.filters.push({ "id": { "$lt": val } }); + return this; + } + + withIdLessThanOrEqualTo(val: any): this { + this.filters.push({ "id": { "$lte": val } }); + return this; + } + + withIdBetween(lower: any, upper: any): this { + this.filters.push({ "id": { "$between": [lower, upper] } }); + return this; + } + + withIdIsKnown(): this { + this.filters.push({ "id": { "$isNull": false } }); + return this; + } + + withIdIsUnknown(): this { + this.filters.push({ "id": { "$isNull": true } }); + return this; + } + + filterByPlatform(val: any): this { + this.filters.push({ "platform": { "$eq": val } }); + return this; + } + + filterByPlatformIn(...vals: any[]): this { + this.filters.push({ "platform": { "$in": vals } }); + return this; + } + + withPlatformIsKnown(): this { + this.filters.push({ "platform": { "$isNull": false } }); + return this; + } + + withPlatformIsUnknown(): this { + this.filters.push({ "platform": { "$isNull": true } }); + return this; + } + + withOrderNumberContaining(val: string): this { + this.filters.push({ "orderNumber": { "$contains": val } }); + return this; + } + + withOrderNumberIs(val: string): this { + this.filters.push({ "orderNumber": { "$eq": val } }); + return this; + } + + withOrderNumberIsNot(val: any): this { + this.filters.push({ "orderNumber": { "$ne": val } }); + return this; + } + + withOrderNumberIn(...vals: any[]): this { + this.filters.push({ "orderNumber": { "$in": vals } }); + return this; + } + + withOrderNumberNotIn(...vals: any[]): this { + this.filters.push({ "orderNumber": { "$notIn": vals } }); + return this; + } + + withOrderNumberGreaterThan(val: any): this { + this.filters.push({ "orderNumber": { "$gt": val } }); + return this; + } + + withOrderNumberGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "orderNumber": { "$gte": val } }); + return this; + } + + withOrderNumberLessThan(val: any): this { + this.filters.push({ "orderNumber": { "$lt": val } }); + return this; + } + + withOrderNumberLessThanOrEqualTo(val: any): this { + this.filters.push({ "orderNumber": { "$lte": val } }); + return this; + } + + withOrderNumberBetween(lower: any, upper: any): this { + this.filters.push({ "orderNumber": { "$between": [lower, upper] } }); + return this; + } + + withOrderNumberIsKnown(): this { + this.filters.push({ "orderNumber": { "$isNull": false } }); + return this; + } + + withOrderNumberIsUnknown(): this { + this.filters.push({ "orderNumber": { "$isNull": true } }); + return this; + } + withOrderNumberNotContaining(val: string): this { + this.filters.push({ "orderNumber": { "$notContains": val } }); + return this; + } + + withOrderNumberStartingWith(val: string): this { + this.filters.push({ "orderNumber": { "$startsWith": val } }); + return this; + } + + withOrderNumberNotStartingWith(val: string): this { + this.filters.push({ "orderNumber": { "$notStartsWith": val } }); + return this; + } + + withOrderNumberEndingWith(val: string): this { + this.filters.push({ "orderNumber": { "$endsWith": val } }); + return this; + } + + withOrderNumberNotEndingWith(val: string): this { + this.filters.push({ "orderNumber": { "$notEndsWith": val } }); + return this; + } + + withOrderNumberSoundingLike(val: string): this { + this.filters.push({ "orderNumber": { "$soundLike": val } }); + return this; + } + + withDescriptionContaining(val: string): this { + this.filters.push({ "description": { "$contains": val } }); + return this; + } + + withDescriptionIs(val: string): this { + this.filters.push({ "description": { "$eq": val } }); + return this; + } + + withDescriptionIsNot(val: any): this { + this.filters.push({ "description": { "$ne": val } }); + return this; + } + + withDescriptionIn(...vals: any[]): this { + this.filters.push({ "description": { "$in": vals } }); + return this; + } + + withDescriptionNotIn(...vals: any[]): this { + this.filters.push({ "description": { "$notIn": vals } }); + return this; + } + + withDescriptionGreaterThan(val: any): this { + this.filters.push({ "description": { "$gt": val } }); + return this; + } + + withDescriptionGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "description": { "$gte": val } }); + return this; + } + + withDescriptionLessThan(val: any): this { + this.filters.push({ "description": { "$lt": val } }); + return this; + } + + withDescriptionLessThanOrEqualTo(val: any): this { + this.filters.push({ "description": { "$lte": val } }); + return this; + } + + withDescriptionBetween(lower: any, upper: any): this { + this.filters.push({ "description": { "$between": [lower, upper] } }); + return this; + } + + withDescriptionIsKnown(): this { + this.filters.push({ "description": { "$isNull": false } }); + return this; + } + + withDescriptionIsUnknown(): this { + this.filters.push({ "description": { "$isNull": true } }); + return this; + } + withDescriptionNotContaining(val: string): this { + this.filters.push({ "description": { "$notContains": val } }); + return this; + } + + withDescriptionStartingWith(val: string): this { + this.filters.push({ "description": { "$startsWith": val } }); + return this; + } + + withDescriptionNotStartingWith(val: string): this { + this.filters.push({ "description": { "$notStartsWith": val } }); + return this; + } + + withDescriptionEndingWith(val: string): this { + this.filters.push({ "description": { "$endsWith": val } }); + return this; + } + + withDescriptionNotEndingWith(val: string): this { + this.filters.push({ "description": { "$notEndsWith": val } }); + return this; + } + + withDescriptionSoundingLike(val: string): this { + this.filters.push({ "description": { "$soundLike": val } }); + return this; + } + + withVersionIs(val: any): this { + this.filters.push({ "version": { "$eq": val } }); + return this; + } + + withVersionIsNot(val: any): this { + this.filters.push({ "version": { "$ne": val } }); + return this; + } + + withVersionIn(...vals: any[]): this { + this.filters.push({ "version": { "$in": vals } }); + return this; + } + + withVersionNotIn(...vals: any[]): this { + this.filters.push({ "version": { "$notIn": vals } }); + return this; + } + + withVersionGreaterThan(val: any): this { + this.filters.push({ "version": { "$gt": val } }); + return this; + } + + withVersionGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "version": { "$gte": val } }); + return this; + } + + withVersionLessThan(val: any): this { + this.filters.push({ "version": { "$lt": val } }); + return this; + } + + withVersionLessThanOrEqualTo(val: any): this { + this.filters.push({ "version": { "$lte": val } }); + return this; + } + + withVersionBetween(lower: any, upper: any): this { + this.filters.push({ "version": { "$between": [lower, upper] } }); + return this; + } + + withVersionIsKnown(): this { + this.filters.push({ "version": { "$isNull": false } }); + return this; + } + + withVersionIsUnknown(): this { + this.filters.push({ "version": { "$isNull": true } }); + return this; + } + + orderByIdAscending(): this { + this.query.orderBy("id", "asc"); + return this; + } + + orderByIdDescending(): this { + this.query.orderBy("id", "desc"); + return this; + } + + + orderByOrderNumberAscending(): this { + this.query.orderBy("orderNumber", "asc"); + return this; + } + + orderByOrderNumberDescending(): this { + this.query.orderBy("orderNumber", "desc"); + return this; + } + + orderByDescriptionAscending(): this { + this.query.orderBy("description", "asc"); + return this; + } + + orderByDescriptionDescending(): this { + this.query.orderBy("description", "desc"); + return this; + } + + orderByVersionAscending(): this { + this.query.orderBy("version", "asc"); + return this; + } + + orderByVersionDescending(): this { + this.query.orderBy("version", "desc"); + return this; + } + + + // --- Aggregations --- + count(): this { + this.query.aggregate("Count", "id", "count"); + this._pageCountAlias = "count"; + this._pageCountMethod = "count"; + return this; + } + + countAs(retName: string): this { + this.query.aggregate("Count", "id", retName); + this._pageCountAlias = retName; + this._pageCountMethod = "countAs"; + return this; + } + + + // --- Group By --- + groupById(): this { + this.query.groupBy("id"); + return this; + } + + groupByIdAs(retName: string): this { + this.query.groupBy("id"); // In TS we don't alias group by yet natively + return this; + } + groupByPlatform(): this { + this.query.groupBy("platform"); + return this; + } + + groupByPlatformAs(retName: string): this { + this.query.groupBy("platform"); // In TS we don't alias group by yet natively + return this; + } + groupByOrderNumber(): this { + this.query.groupBy("orderNumber"); + return this; + } + + groupByOrderNumberAs(retName: string): this { + this.query.groupBy("orderNumber"); // In TS we don't alias group by yet natively + return this; + } + groupByDescription(): this { + this.query.groupBy("description"); + return this; + } + + groupByDescriptionAs(retName: string): this { + this.query.groupBy("description"); // In TS we don't alias group by yet natively + return this; + } + groupByVersion(): this { + this.query.groupBy("version"); + return this; + } + + groupByVersionAs(retName: string): this { + this.query.groupBy("version"); // In TS we don't alias group by yet natively + return this; + } + + // --- Facets --- + facetByPlatformAs( + facetName: string, + request: { toQuery(): SelectQuery }, + includeAllFacets = true, + ): this { + this.query.facetBy(facetName, "platform", request, includeAllFacets, + { kind: "facet", method: "facetByPlatformAs", relation: "platform", alias: facetName }); + return this; + } + + + private async executeForListInternal(context: UserContext): Promise> { + this.ensureIntent(); + if (this._pageCountAlias !== undefined) return this.executeCountedListInternal(context); + if (this.filters.length > 0) { + this.query.filter({ "$and": this.filters }); + } + + const service = context.requireResource("dataService"); + const rows = await service.executeQuery(context.prepareQuery(this.query)); + const aggregateOnly = this.query.aggregateItems.length > 0 && this.query.groupByItems.length === 0; + const data = aggregateOnly ? [] : rows.map((row: unknown) => + row instanceof CustomerOrder + ? row + : CustomerOrder.fromRecord(row as Record, new EntityRoot())); + const result = new SmartList(data); + if (aggregateOnly && rows[0]) Object.assign(result.aggregations, rows[0]); + + if (this.query.facets.length > 0) { + result.facets = await executeRelationFacets( + service, (query: any) => context.prepareQuery(query), this.query, this.query.facets); + } + + return result; + } + + private async executeForRowsInternal(context: UserContext): Promise>> { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + return new SmartList>( + await service.executeQuery(context.prepareQuery(this.query)) as Record[]); + } + + private async executeCountedListInternal( + context: UserContext, + ): Promise> { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + const preparedQuery = context.prepareQuery(this.query); + const useIdSet = preparedQuery.localIdSetPaginationOptions() !== undefined; + const countOperation = { kind: "named-count" as const, + method: this._pageCountMethod ?? "count", + ...(this._pageCountMethod === "countAs" ? { alias: this._pageCountAlias } : {}) }; + const totalCountBeforeRows = useIdSet ? undefined : await service.executeCount( + preparedQuery, this._pageCountAlias, countOperation); + const pageQuery = preparedQuery.clone(); + // A page count describes the full filtered set, not an aggregate-only page row. + pageQuery.aggregateItems = pageQuery.aggregateItems.filter(item => + !(String(item.function).toLowerCase() === "count" && item.alias === this._pageCountAlias)); + const rows = await service.executeQuery(pageQuery); + const totalCount = useIdSet && context.idSetPaginationCountAccuracy === "EXACT" + ? context.idSetPaginationCount! + : (totalCountBeforeRows ?? await service.executeCount(preparedQuery, this._pageCountAlias, countOperation)); + const data = new SmartList(rows.map((row: unknown) => + row instanceof CustomerOrder + ? row + : CustomerOrder.fromRecord(row as Record, new EntityRoot()))); + data.totalCount = totalCount; + if (this._pageCountAlias !== undefined) data.aggregations[this._pageCountAlias] = totalCount; + if (this.query.facets.length > 0) { + data.facets = await executeRelationFacets( + service, (query: any) => context.prepareQuery(query), preparedQuery, preparedQuery.facets); + } + return data; + } + + private executeForStreamInternal(context: UserContext, chunkSize: number): AsyncIterable { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + const preparedQuery = context.prepareQuery(this.query); + // Prepare Context and capture the runtime request before returning a lazy iterator. + const chunks = service.executeForStream(preparedQuery, chunkSize); + return (async function* () { + for await (const chunk of chunks) { + for (const entity of chunk) { + yield entity instanceof CustomerOrder + ? entity + : CustomerOrder.fromRecord(entity as Record, new EntityRoot()); + } + } + })(); + } + + private ensureIntent(): void { + // Runtime owns validation, Unicode whitespace rules and typed diagnostics. + new QueryIntent(this._comment, this._purpose); + } + +} + +export class ExecutableCustomerOrderRequest { + constructor( + private readonly execute: (context: UserContext) => Promise>, + private readonly executeRows: (context: UserContext) => Promise>>, + private readonly stream: (context: UserContext, chunkSize: number) => AsyncIterable, + private readonly limitOne: () => void, + private readonly addComment: (comment: string) => void, + private readonly ensureIntent: () => void, + ) {} + + comment(c: string): this { + this.addComment(c); + return this; + } + + newEntity(context: UserContext): CustomerOrder { + this.ensureIntent(); + return new CustomerOrder(); + } + + executeForList(context: UserContext): Promise> { + this.ensureIntent(); + return this.execute(context); + } + + executeForRows(context: UserContext): Promise>> { + this.ensureIntent(); + return this.executeRows(context); + } + + executeForStream(context: UserContext, chunkSize = 1000): AsyncIterable { + this.ensureIntent(); + return this.stream(context, chunkSize); + } + + async executeForOne(context: UserContext): Promise { + this.limitOne(); + return (await this.executeForList(context))[0]; + } +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/OrderItemRequest.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/OrderItemRequest.ts new file mode 100644 index 0000000000000000000000000000000000000000..71354b80c79899c464c186c8532d4a0e47fc2594 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/OrderItemRequest.ts @@ -0,0 +1,611 @@ +import { EntityRoot, SelectQuery, SmartList, TeaQLDataService, UserContext, executeRelationFacets } from '../../teaql-ts'; +import { OrderItem } from '../models/OrderItem'; +import { mergeGeneratedScalarSearch, DynamicSearchWarning, QueryIntent } from 'teaql-ts'; +import { GENERATED_SEARCH_MODELS } from '../../generated-search-models'; + + + +export class OrderItemRequest { + private query: SelectQuery; + private filters: any[] = []; + private _purpose: string | undefined; + private _comment: string | undefined; + private _pageCountAlias: string | undefined; + private _pageCountMethod: "count" | "countAs" | undefined; + private _dynamicSearchWarnings: DynamicSearchWarning[] = []; + + constructor(minimal = false) { + this.query = new SelectQuery("OrderItem"); + this.filters.push({ version: { "$gte": 1 } }); + if (minimal) { + this.selectId(); + this.selectVersion(); + } + else this.selectSelfFields(); + } + + comment(c: string): this { + this.query.comment(c); + this._comment = c; + return this; + } + + filterByJson(source: unknown, warn?: (warning: DynamicSearchWarning) => void): this { + const base = this.query.clone(); + if (this.filters.length > 0) base.filter({ "$and": this.filters }); + const merged = mergeGeneratedScalarSearch(base, source, GENERATED_SEARCH_MODELS, warn); + this.query = merged.query as SelectQuery; + this.filters = this.query.filterCondition ? [this.query.filterCondition] : []; + this._dynamicSearchWarnings.push(...merged.warnings); + return this; + } + + get dynamicSearchWarnings(): readonly DynamicSearchWarning[] { + return this._dynamicSearchWarnings.map(warning => ({ ...warning })); + } + + purpose(p: string): ExecutableOrderItemRequest { + this.query.purpose(p); + this._purpose = p; + return new ExecutableOrderItemRequest( + (context) => context.withQueryRequest(this.query, () => this.executeForListInternal(context)), + (context) => context.withQueryRequest(this.query, () => this.executeForRowsInternal(context)), + (context, chunkSize) => this.executeForStreamInternal(context, chunkSize), + () => this.limit(1), + (c) => this.comment(c), + () => this.ensureIntent()); + } + + optimizeForContinuousPageFetch(): this { + this.query.optimizeForContinuousPageFetch(); + return this; + } + + optimizeForContinuousPageFetchWith(namespace: string, ttlSeconds: number): this { + this.query.optimizeForContinuousPageFetchWith(namespace, ttlSeconds); + return this; + } + + optimizePaginationWithIdSet(): this { + this.query.optimizePaginationWithIdSet(); + return this; + } + + optimizePaginationWithIdSetConfig(namespace: string, ttlSeconds: number, maxIds: number): this { + this.query.optimizePaginationWithIdSetConfig(namespace, ttlSeconds, maxIds); + return this; + } + + topNProbeParentThreshold(threshold: number): this { + this.query.topNProbeParentThreshold(threshold); + return this; + } + + limit(n: number): this { + this.query.limit(n); + return this; + } + + offset(n: number): this { + this.query.offset(n); + return this; + } + + toQuery(): SelectQuery { + if (this.filters.length > 0) { + this.query.filter({ "$and": this.filters }); + } + return this.query; + } + + withDeletedRows(): this { + this.filters = this.filters.filter(filter => !("version" in filter)); + return this; + } + + deletedRowsOnly(): this { + this.withDeletedRows(); + this.filters.push({ version: { "$lte": -1 } }); + return this; + } + + selectSelfFields(): this { + this.query.select(["id", "customerOrder", "name", "version"]); + return this; + } + + selectId(): this { + this.query.select(["id"]); + return this; + } + + + selectName(): this { + this.query.select(["name"]); + return this; + } + + selectVersion(): this { + this.query.select(["version"]); + return this; + } + + + selectCustomerOrderWith(request: { toQuery(): SelectQuery }): this { + this.query.select(["customerOrder"]); + this.query.relationQuery( + "customerOrder", request.toQuery(), "customerOrder", "id", false, + { kind: "relation-select", method: "selectCustomerOrderWith", relation: "customerOrder" }); + return this; + } + + withCustomerOrderMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "customerOrder": { + "$inSubquery": { query: request.toQuery(), field: "id" }, + }, + }); + return this; + } + + withoutCustomerOrderMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "customerOrder": { + "$notInSubquery": { query: request.toQuery(), field: "id" }, + }, + }); + return this; + } + + + + + withIdIs(val: any): this { + this.filters.push({ "id": { "$eq": val } }); + return this; + } + + withIdIsNot(val: any): this { + this.filters.push({ "id": { "$ne": val } }); + return this; + } + + withIdIn(...vals: any[]): this { + this.filters.push({ "id": { "$in": vals } }); + return this; + } + + withIdNotIn(...vals: any[]): this { + this.filters.push({ "id": { "$notIn": vals } }); + return this; + } + + withIdGreaterThan(val: any): this { + this.filters.push({ "id": { "$gt": val } }); + return this; + } + + withIdGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "id": { "$gte": val } }); + return this; + } + + withIdLessThan(val: any): this { + this.filters.push({ "id": { "$lt": val } }); + return this; + } + + withIdLessThanOrEqualTo(val: any): this { + this.filters.push({ "id": { "$lte": val } }); + return this; + } + + withIdBetween(lower: any, upper: any): this { + this.filters.push({ "id": { "$between": [lower, upper] } }); + return this; + } + + withIdIsKnown(): this { + this.filters.push({ "id": { "$isNull": false } }); + return this; + } + + withIdIsUnknown(): this { + this.filters.push({ "id": { "$isNull": true } }); + return this; + } + + filterByCustomerOrder(val: any): this { + this.filters.push({ "customerOrder": { "$eq": val } }); + return this; + } + + filterByCustomerOrderIn(...vals: any[]): this { + this.filters.push({ "customerOrder": { "$in": vals } }); + return this; + } + + withCustomerOrderIsKnown(): this { + this.filters.push({ "customerOrder": { "$isNull": false } }); + return this; + } + + withCustomerOrderIsUnknown(): this { + this.filters.push({ "customerOrder": { "$isNull": true } }); + return this; + } + + withNameContaining(val: string): this { + this.filters.push({ "name": { "$contains": val } }); + return this; + } + + withNameIs(val: string): this { + this.filters.push({ "name": { "$eq": val } }); + return this; + } + + withNameIsNot(val: any): this { + this.filters.push({ "name": { "$ne": val } }); + return this; + } + + withNameIn(...vals: any[]): this { + this.filters.push({ "name": { "$in": vals } }); + return this; + } + + withNameNotIn(...vals: any[]): this { + this.filters.push({ "name": { "$notIn": vals } }); + return this; + } + + withNameGreaterThan(val: any): this { + this.filters.push({ "name": { "$gt": val } }); + return this; + } + + withNameGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "name": { "$gte": val } }); + return this; + } + + withNameLessThan(val: any): this { + this.filters.push({ "name": { "$lt": val } }); + return this; + } + + withNameLessThanOrEqualTo(val: any): this { + this.filters.push({ "name": { "$lte": val } }); + return this; + } + + withNameBetween(lower: any, upper: any): this { + this.filters.push({ "name": { "$between": [lower, upper] } }); + return this; + } + + withNameIsKnown(): this { + this.filters.push({ "name": { "$isNull": false } }); + return this; + } + + withNameIsUnknown(): this { + this.filters.push({ "name": { "$isNull": true } }); + return this; + } + withNameNotContaining(val: string): this { + this.filters.push({ "name": { "$notContains": val } }); + return this; + } + + withNameStartingWith(val: string): this { + this.filters.push({ "name": { "$startsWith": val } }); + return this; + } + + withNameNotStartingWith(val: string): this { + this.filters.push({ "name": { "$notStartsWith": val } }); + return this; + } + + withNameEndingWith(val: string): this { + this.filters.push({ "name": { "$endsWith": val } }); + return this; + } + + withNameNotEndingWith(val: string): this { + this.filters.push({ "name": { "$notEndsWith": val } }); + return this; + } + + withNameSoundingLike(val: string): this { + this.filters.push({ "name": { "$soundLike": val } }); + return this; + } + + withVersionIs(val: any): this { + this.filters.push({ "version": { "$eq": val } }); + return this; + } + + withVersionIsNot(val: any): this { + this.filters.push({ "version": { "$ne": val } }); + return this; + } + + withVersionIn(...vals: any[]): this { + this.filters.push({ "version": { "$in": vals } }); + return this; + } + + withVersionNotIn(...vals: any[]): this { + this.filters.push({ "version": { "$notIn": vals } }); + return this; + } + + withVersionGreaterThan(val: any): this { + this.filters.push({ "version": { "$gt": val } }); + return this; + } + + withVersionGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "version": { "$gte": val } }); + return this; + } + + withVersionLessThan(val: any): this { + this.filters.push({ "version": { "$lt": val } }); + return this; + } + + withVersionLessThanOrEqualTo(val: any): this { + this.filters.push({ "version": { "$lte": val } }); + return this; + } + + withVersionBetween(lower: any, upper: any): this { + this.filters.push({ "version": { "$between": [lower, upper] } }); + return this; + } + + withVersionIsKnown(): this { + this.filters.push({ "version": { "$isNull": false } }); + return this; + } + + withVersionIsUnknown(): this { + this.filters.push({ "version": { "$isNull": true } }); + return this; + } + + orderByIdAscending(): this { + this.query.orderBy("id", "asc"); + return this; + } + + orderByIdDescending(): this { + this.query.orderBy("id", "desc"); + return this; + } + + + orderByNameAscending(): this { + this.query.orderBy("name", "asc"); + return this; + } + + orderByNameDescending(): this { + this.query.orderBy("name", "desc"); + return this; + } + + orderByVersionAscending(): this { + this.query.orderBy("version", "asc"); + return this; + } + + orderByVersionDescending(): this { + this.query.orderBy("version", "desc"); + return this; + } + + + // --- Aggregations --- + count(): this { + this.query.aggregate("Count", "id", "count"); + this._pageCountAlias = "count"; + this._pageCountMethod = "count"; + return this; + } + + countAs(retName: string): this { + this.query.aggregate("Count", "id", retName); + this._pageCountAlias = retName; + this._pageCountMethod = "countAs"; + return this; + } + + + // --- Group By --- + groupById(): this { + this.query.groupBy("id"); + return this; + } + + groupByIdAs(retName: string): this { + this.query.groupBy("id"); // In TS we don't alias group by yet natively + return this; + } + groupByCustomerOrder(): this { + this.query.groupBy("customerOrder"); + return this; + } + + groupByCustomerOrderAs(retName: string): this { + this.query.groupBy("customerOrder"); // In TS we don't alias group by yet natively + return this; + } + groupByName(): this { + this.query.groupBy("name"); + return this; + } + + groupByNameAs(retName: string): this { + this.query.groupBy("name"); // In TS we don't alias group by yet natively + return this; + } + groupByVersion(): this { + this.query.groupBy("version"); + return this; + } + + groupByVersionAs(retName: string): this { + this.query.groupBy("version"); // In TS we don't alias group by yet natively + return this; + } + + // --- Facets --- + facetByCustomerOrderAs( + facetName: string, + request: { toQuery(): SelectQuery }, + includeAllFacets = true, + ): this { + this.query.facetBy(facetName, "customerOrder", request, includeAllFacets, + { kind: "facet", method: "facetByCustomerOrderAs", relation: "customerOrder", alias: facetName }); + return this; + } + + + private async executeForListInternal(context: UserContext): Promise> { + this.ensureIntent(); + if (this._pageCountAlias !== undefined) return this.executeCountedListInternal(context); + if (this.filters.length > 0) { + this.query.filter({ "$and": this.filters }); + } + + const service = context.requireResource("dataService"); + const rows = await service.executeQuery(context.prepareQuery(this.query)); + const aggregateOnly = this.query.aggregateItems.length > 0 && this.query.groupByItems.length === 0; + const data = aggregateOnly ? [] : rows.map((row: unknown) => + row instanceof OrderItem + ? row + : OrderItem.fromRecord(row as Record, new EntityRoot())); + const result = new SmartList(data); + if (aggregateOnly && rows[0]) Object.assign(result.aggregations, rows[0]); + + if (this.query.facets.length > 0) { + result.facets = await executeRelationFacets( + service, (query: any) => context.prepareQuery(query), this.query, this.query.facets); + } + + return result; + } + + private async executeForRowsInternal(context: UserContext): Promise>> { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + return new SmartList>( + await service.executeQuery(context.prepareQuery(this.query)) as Record[]); + } + + private async executeCountedListInternal( + context: UserContext, + ): Promise> { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + const preparedQuery = context.prepareQuery(this.query); + const useIdSet = preparedQuery.localIdSetPaginationOptions() !== undefined; + const countOperation = { kind: "named-count" as const, + method: this._pageCountMethod ?? "count", + ...(this._pageCountMethod === "countAs" ? { alias: this._pageCountAlias } : {}) }; + const totalCountBeforeRows = useIdSet ? undefined : await service.executeCount( + preparedQuery, this._pageCountAlias, countOperation); + const pageQuery = preparedQuery.clone(); + // A page count describes the full filtered set, not an aggregate-only page row. + pageQuery.aggregateItems = pageQuery.aggregateItems.filter(item => + !(String(item.function).toLowerCase() === "count" && item.alias === this._pageCountAlias)); + const rows = await service.executeQuery(pageQuery); + const totalCount = useIdSet && context.idSetPaginationCountAccuracy === "EXACT" + ? context.idSetPaginationCount! + : (totalCountBeforeRows ?? await service.executeCount(preparedQuery, this._pageCountAlias, countOperation)); + const data = new SmartList(rows.map((row: unknown) => + row instanceof OrderItem + ? row + : OrderItem.fromRecord(row as Record, new EntityRoot()))); + data.totalCount = totalCount; + if (this._pageCountAlias !== undefined) data.aggregations[this._pageCountAlias] = totalCount; + if (this.query.facets.length > 0) { + data.facets = await executeRelationFacets( + service, (query: any) => context.prepareQuery(query), preparedQuery, preparedQuery.facets); + } + return data; + } + + private executeForStreamInternal(context: UserContext, chunkSize: number): AsyncIterable { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + const preparedQuery = context.prepareQuery(this.query); + // Prepare Context and capture the runtime request before returning a lazy iterator. + const chunks = service.executeForStream(preparedQuery, chunkSize); + return (async function* () { + for await (const chunk of chunks) { + for (const entity of chunk) { + yield entity instanceof OrderItem + ? entity + : OrderItem.fromRecord(entity as Record, new EntityRoot()); + } + } + })(); + } + + private ensureIntent(): void { + // Runtime owns validation, Unicode whitespace rules and typed diagnostics. + new QueryIntent(this._comment, this._purpose); + } + +} + +export class ExecutableOrderItemRequest { + constructor( + private readonly execute: (context: UserContext) => Promise>, + private readonly executeRows: (context: UserContext) => Promise>>, + private readonly stream: (context: UserContext, chunkSize: number) => AsyncIterable, + private readonly limitOne: () => void, + private readonly addComment: (comment: string) => void, + private readonly ensureIntent: () => void, + ) {} + + comment(c: string): this { + this.addComment(c); + return this; + } + + newEntity(context: UserContext): OrderItem { + this.ensureIntent(); + return new OrderItem(); + } + + executeForList(context: UserContext): Promise> { + this.ensureIntent(); + return this.execute(context); + } + + executeForRows(context: UserContext): Promise>> { + this.ensureIntent(); + return this.executeRows(context); + } + + executeForStream(context: UserContext, chunkSize = 1000): AsyncIterable { + this.ensureIntent(); + return this.stream(context, chunkSize); + } + + async executeForOne(context: UserContext): Promise { + this.limitOne(); + return (await this.executeForList(context))[0]; + } +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/PaymentAttemptRequest.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/PaymentAttemptRequest.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e732e407d2db3759f7de8fe4c91828b36f27a6e --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/PaymentAttemptRequest.ts @@ -0,0 +1,611 @@ +import { EntityRoot, SelectQuery, SmartList, TeaQLDataService, UserContext, executeRelationFacets } from '../../teaql-ts'; +import { PaymentAttempt } from '../models/PaymentAttempt'; +import { mergeGeneratedScalarSearch, DynamicSearchWarning, QueryIntent } from 'teaql-ts'; +import { GENERATED_SEARCH_MODELS } from '../../generated-search-models'; + + + +export class PaymentAttemptRequest { + private query: SelectQuery; + private filters: any[] = []; + private _purpose: string | undefined; + private _comment: string | undefined; + private _pageCountAlias: string | undefined; + private _pageCountMethod: "count" | "countAs" | undefined; + private _dynamicSearchWarnings: DynamicSearchWarning[] = []; + + constructor(minimal = false) { + this.query = new SelectQuery("PaymentAttempt"); + this.filters.push({ version: { "$gte": 1 } }); + if (minimal) { + this.selectId(); + this.selectVersion(); + } + else this.selectSelfFields(); + } + + comment(c: string): this { + this.query.comment(c); + this._comment = c; + return this; + } + + filterByJson(source: unknown, warn?: (warning: DynamicSearchWarning) => void): this { + const base = this.query.clone(); + if (this.filters.length > 0) base.filter({ "$and": this.filters }); + const merged = mergeGeneratedScalarSearch(base, source, GENERATED_SEARCH_MODELS, warn); + this.query = merged.query as SelectQuery; + this.filters = this.query.filterCondition ? [this.query.filterCondition] : []; + this._dynamicSearchWarnings.push(...merged.warnings); + return this; + } + + get dynamicSearchWarnings(): readonly DynamicSearchWarning[] { + return this._dynamicSearchWarnings.map(warning => ({ ...warning })); + } + + purpose(p: string): ExecutablePaymentAttemptRequest { + this.query.purpose(p); + this._purpose = p; + return new ExecutablePaymentAttemptRequest( + (context) => context.withQueryRequest(this.query, () => this.executeForListInternal(context)), + (context) => context.withQueryRequest(this.query, () => this.executeForRowsInternal(context)), + (context, chunkSize) => this.executeForStreamInternal(context, chunkSize), + () => this.limit(1), + (c) => this.comment(c), + () => this.ensureIntent()); + } + + optimizeForContinuousPageFetch(): this { + this.query.optimizeForContinuousPageFetch(); + return this; + } + + optimizeForContinuousPageFetchWith(namespace: string, ttlSeconds: number): this { + this.query.optimizeForContinuousPageFetchWith(namespace, ttlSeconds); + return this; + } + + optimizePaginationWithIdSet(): this { + this.query.optimizePaginationWithIdSet(); + return this; + } + + optimizePaginationWithIdSetConfig(namespace: string, ttlSeconds: number, maxIds: number): this { + this.query.optimizePaginationWithIdSetConfig(namespace, ttlSeconds, maxIds); + return this; + } + + topNProbeParentThreshold(threshold: number): this { + this.query.topNProbeParentThreshold(threshold); + return this; + } + + limit(n: number): this { + this.query.limit(n); + return this; + } + + offset(n: number): this { + this.query.offset(n); + return this; + } + + toQuery(): SelectQuery { + if (this.filters.length > 0) { + this.query.filter({ "$and": this.filters }); + } + return this.query; + } + + withDeletedRows(): this { + this.filters = this.filters.filter(filter => !("version" in filter)); + return this; + } + + deletedRowsOnly(): this { + this.withDeletedRows(); + this.filters.push({ version: { "$lte": -1 } }); + return this; + } + + selectSelfFields(): this { + this.query.select(["id", "payment", "referenceCode", "version"]); + return this; + } + + selectId(): this { + this.query.select(["id"]); + return this; + } + + + selectReferenceCode(): this { + this.query.select(["referenceCode"]); + return this; + } + + selectVersion(): this { + this.query.select(["version"]); + return this; + } + + + selectPaymentWith(request: { toQuery(): SelectQuery }): this { + this.query.select(["payment"]); + this.query.relationQuery( + "payment", request.toQuery(), "payment", "id", false, + { kind: "relation-select", method: "selectPaymentWith", relation: "payment" }); + return this; + } + + withPaymentMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "payment": { + "$inSubquery": { query: request.toQuery(), field: "id" }, + }, + }); + return this; + } + + withoutPaymentMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "payment": { + "$notInSubquery": { query: request.toQuery(), field: "id" }, + }, + }); + return this; + } + + + + + withIdIs(val: any): this { + this.filters.push({ "id": { "$eq": val } }); + return this; + } + + withIdIsNot(val: any): this { + this.filters.push({ "id": { "$ne": val } }); + return this; + } + + withIdIn(...vals: any[]): this { + this.filters.push({ "id": { "$in": vals } }); + return this; + } + + withIdNotIn(...vals: any[]): this { + this.filters.push({ "id": { "$notIn": vals } }); + return this; + } + + withIdGreaterThan(val: any): this { + this.filters.push({ "id": { "$gt": val } }); + return this; + } + + withIdGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "id": { "$gte": val } }); + return this; + } + + withIdLessThan(val: any): this { + this.filters.push({ "id": { "$lt": val } }); + return this; + } + + withIdLessThanOrEqualTo(val: any): this { + this.filters.push({ "id": { "$lte": val } }); + return this; + } + + withIdBetween(lower: any, upper: any): this { + this.filters.push({ "id": { "$between": [lower, upper] } }); + return this; + } + + withIdIsKnown(): this { + this.filters.push({ "id": { "$isNull": false } }); + return this; + } + + withIdIsUnknown(): this { + this.filters.push({ "id": { "$isNull": true } }); + return this; + } + + filterByPayment(val: any): this { + this.filters.push({ "payment": { "$eq": val } }); + return this; + } + + filterByPaymentIn(...vals: any[]): this { + this.filters.push({ "payment": { "$in": vals } }); + return this; + } + + withPaymentIsKnown(): this { + this.filters.push({ "payment": { "$isNull": false } }); + return this; + } + + withPaymentIsUnknown(): this { + this.filters.push({ "payment": { "$isNull": true } }); + return this; + } + + withReferenceCodeContaining(val: string): this { + this.filters.push({ "referenceCode": { "$contains": val } }); + return this; + } + + withReferenceCodeIs(val: string): this { + this.filters.push({ "referenceCode": { "$eq": val } }); + return this; + } + + withReferenceCodeIsNot(val: any): this { + this.filters.push({ "referenceCode": { "$ne": val } }); + return this; + } + + withReferenceCodeIn(...vals: any[]): this { + this.filters.push({ "referenceCode": { "$in": vals } }); + return this; + } + + withReferenceCodeNotIn(...vals: any[]): this { + this.filters.push({ "referenceCode": { "$notIn": vals } }); + return this; + } + + withReferenceCodeGreaterThan(val: any): this { + this.filters.push({ "referenceCode": { "$gt": val } }); + return this; + } + + withReferenceCodeGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "referenceCode": { "$gte": val } }); + return this; + } + + withReferenceCodeLessThan(val: any): this { + this.filters.push({ "referenceCode": { "$lt": val } }); + return this; + } + + withReferenceCodeLessThanOrEqualTo(val: any): this { + this.filters.push({ "referenceCode": { "$lte": val } }); + return this; + } + + withReferenceCodeBetween(lower: any, upper: any): this { + this.filters.push({ "referenceCode": { "$between": [lower, upper] } }); + return this; + } + + withReferenceCodeIsKnown(): this { + this.filters.push({ "referenceCode": { "$isNull": false } }); + return this; + } + + withReferenceCodeIsUnknown(): this { + this.filters.push({ "referenceCode": { "$isNull": true } }); + return this; + } + withReferenceCodeNotContaining(val: string): this { + this.filters.push({ "referenceCode": { "$notContains": val } }); + return this; + } + + withReferenceCodeStartingWith(val: string): this { + this.filters.push({ "referenceCode": { "$startsWith": val } }); + return this; + } + + withReferenceCodeNotStartingWith(val: string): this { + this.filters.push({ "referenceCode": { "$notStartsWith": val } }); + return this; + } + + withReferenceCodeEndingWith(val: string): this { + this.filters.push({ "referenceCode": { "$endsWith": val } }); + return this; + } + + withReferenceCodeNotEndingWith(val: string): this { + this.filters.push({ "referenceCode": { "$notEndsWith": val } }); + return this; + } + + withReferenceCodeSoundingLike(val: string): this { + this.filters.push({ "referenceCode": { "$soundLike": val } }); + return this; + } + + withVersionIs(val: any): this { + this.filters.push({ "version": { "$eq": val } }); + return this; + } + + withVersionIsNot(val: any): this { + this.filters.push({ "version": { "$ne": val } }); + return this; + } + + withVersionIn(...vals: any[]): this { + this.filters.push({ "version": { "$in": vals } }); + return this; + } + + withVersionNotIn(...vals: any[]): this { + this.filters.push({ "version": { "$notIn": vals } }); + return this; + } + + withVersionGreaterThan(val: any): this { + this.filters.push({ "version": { "$gt": val } }); + return this; + } + + withVersionGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "version": { "$gte": val } }); + return this; + } + + withVersionLessThan(val: any): this { + this.filters.push({ "version": { "$lt": val } }); + return this; + } + + withVersionLessThanOrEqualTo(val: any): this { + this.filters.push({ "version": { "$lte": val } }); + return this; + } + + withVersionBetween(lower: any, upper: any): this { + this.filters.push({ "version": { "$between": [lower, upper] } }); + return this; + } + + withVersionIsKnown(): this { + this.filters.push({ "version": { "$isNull": false } }); + return this; + } + + withVersionIsUnknown(): this { + this.filters.push({ "version": { "$isNull": true } }); + return this; + } + + orderByIdAscending(): this { + this.query.orderBy("id", "asc"); + return this; + } + + orderByIdDescending(): this { + this.query.orderBy("id", "desc"); + return this; + } + + + orderByReferenceCodeAscending(): this { + this.query.orderBy("referenceCode", "asc"); + return this; + } + + orderByReferenceCodeDescending(): this { + this.query.orderBy("referenceCode", "desc"); + return this; + } + + orderByVersionAscending(): this { + this.query.orderBy("version", "asc"); + return this; + } + + orderByVersionDescending(): this { + this.query.orderBy("version", "desc"); + return this; + } + + + // --- Aggregations --- + count(): this { + this.query.aggregate("Count", "id", "count"); + this._pageCountAlias = "count"; + this._pageCountMethod = "count"; + return this; + } + + countAs(retName: string): this { + this.query.aggregate("Count", "id", retName); + this._pageCountAlias = retName; + this._pageCountMethod = "countAs"; + return this; + } + + + // --- Group By --- + groupById(): this { + this.query.groupBy("id"); + return this; + } + + groupByIdAs(retName: string): this { + this.query.groupBy("id"); // In TS we don't alias group by yet natively + return this; + } + groupByPayment(): this { + this.query.groupBy("payment"); + return this; + } + + groupByPaymentAs(retName: string): this { + this.query.groupBy("payment"); // In TS we don't alias group by yet natively + return this; + } + groupByReferenceCode(): this { + this.query.groupBy("referenceCode"); + return this; + } + + groupByReferenceCodeAs(retName: string): this { + this.query.groupBy("referenceCode"); // In TS we don't alias group by yet natively + return this; + } + groupByVersion(): this { + this.query.groupBy("version"); + return this; + } + + groupByVersionAs(retName: string): this { + this.query.groupBy("version"); // In TS we don't alias group by yet natively + return this; + } + + // --- Facets --- + facetByPaymentAs( + facetName: string, + request: { toQuery(): SelectQuery }, + includeAllFacets = true, + ): this { + this.query.facetBy(facetName, "payment", request, includeAllFacets, + { kind: "facet", method: "facetByPaymentAs", relation: "payment", alias: facetName }); + return this; + } + + + private async executeForListInternal(context: UserContext): Promise> { + this.ensureIntent(); + if (this._pageCountAlias !== undefined) return this.executeCountedListInternal(context); + if (this.filters.length > 0) { + this.query.filter({ "$and": this.filters }); + } + + const service = context.requireResource("dataService"); + const rows = await service.executeQuery(context.prepareQuery(this.query)); + const aggregateOnly = this.query.aggregateItems.length > 0 && this.query.groupByItems.length === 0; + const data = aggregateOnly ? [] : rows.map((row: unknown) => + row instanceof PaymentAttempt + ? row + : PaymentAttempt.fromRecord(row as Record, new EntityRoot())); + const result = new SmartList(data); + if (aggregateOnly && rows[0]) Object.assign(result.aggregations, rows[0]); + + if (this.query.facets.length > 0) { + result.facets = await executeRelationFacets( + service, (query: any) => context.prepareQuery(query), this.query, this.query.facets); + } + + return result; + } + + private async executeForRowsInternal(context: UserContext): Promise>> { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + return new SmartList>( + await service.executeQuery(context.prepareQuery(this.query)) as Record[]); + } + + private async executeCountedListInternal( + context: UserContext, + ): Promise> { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + const preparedQuery = context.prepareQuery(this.query); + const useIdSet = preparedQuery.localIdSetPaginationOptions() !== undefined; + const countOperation = { kind: "named-count" as const, + method: this._pageCountMethod ?? "count", + ...(this._pageCountMethod === "countAs" ? { alias: this._pageCountAlias } : {}) }; + const totalCountBeforeRows = useIdSet ? undefined : await service.executeCount( + preparedQuery, this._pageCountAlias, countOperation); + const pageQuery = preparedQuery.clone(); + // A page count describes the full filtered set, not an aggregate-only page row. + pageQuery.aggregateItems = pageQuery.aggregateItems.filter(item => + !(String(item.function).toLowerCase() === "count" && item.alias === this._pageCountAlias)); + const rows = await service.executeQuery(pageQuery); + const totalCount = useIdSet && context.idSetPaginationCountAccuracy === "EXACT" + ? context.idSetPaginationCount! + : (totalCountBeforeRows ?? await service.executeCount(preparedQuery, this._pageCountAlias, countOperation)); + const data = new SmartList(rows.map((row: unknown) => + row instanceof PaymentAttempt + ? row + : PaymentAttempt.fromRecord(row as Record, new EntityRoot()))); + data.totalCount = totalCount; + if (this._pageCountAlias !== undefined) data.aggregations[this._pageCountAlias] = totalCount; + if (this.query.facets.length > 0) { + data.facets = await executeRelationFacets( + service, (query: any) => context.prepareQuery(query), preparedQuery, preparedQuery.facets); + } + return data; + } + + private executeForStreamInternal(context: UserContext, chunkSize: number): AsyncIterable { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + const preparedQuery = context.prepareQuery(this.query); + // Prepare Context and capture the runtime request before returning a lazy iterator. + const chunks = service.executeForStream(preparedQuery, chunkSize); + return (async function* () { + for await (const chunk of chunks) { + for (const entity of chunk) { + yield entity instanceof PaymentAttempt + ? entity + : PaymentAttempt.fromRecord(entity as Record, new EntityRoot()); + } + } + })(); + } + + private ensureIntent(): void { + // Runtime owns validation, Unicode whitespace rules and typed diagnostics. + new QueryIntent(this._comment, this._purpose); + } + +} + +export class ExecutablePaymentAttemptRequest { + constructor( + private readonly execute: (context: UserContext) => Promise>, + private readonly executeRows: (context: UserContext) => Promise>>, + private readonly stream: (context: UserContext, chunkSize: number) => AsyncIterable, + private readonly limitOne: () => void, + private readonly addComment: (comment: string) => void, + private readonly ensureIntent: () => void, + ) {} + + comment(c: string): this { + this.addComment(c); + return this; + } + + newEntity(context: UserContext): PaymentAttempt { + this.ensureIntent(); + return new PaymentAttempt(); + } + + executeForList(context: UserContext): Promise> { + this.ensureIntent(); + return this.execute(context); + } + + executeForRows(context: UserContext): Promise>> { + this.ensureIntent(); + return this.executeRows(context); + } + + executeForStream(context: UserContext, chunkSize = 1000): AsyncIterable { + this.ensureIntent(); + return this.stream(context, chunkSize); + } + + async executeForOne(context: UserContext): Promise { + this.limitOne(); + return (await this.executeForList(context))[0]; + } +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/PaymentRequest.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/PaymentRequest.ts new file mode 100644 index 0000000000000000000000000000000000000000..c7026402470891bb8738f59c0d7a99be8682dfdd --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/PaymentRequest.ts @@ -0,0 +1,672 @@ +import { EntityRoot, SelectQuery, SmartList, TeaQLDataService, UserContext, executeRelationFacets } from '../../teaql-ts'; +import { Payment } from '../models/Payment'; +import { mergeGeneratedScalarSearch, DynamicSearchWarning, QueryIntent } from 'teaql-ts'; +import { GENERATED_SEARCH_MODELS } from '../../generated-search-models'; + + + +export class PaymentRequest { + private query: SelectQuery; + private filters: any[] = []; + private _purpose: string | undefined; + private _comment: string | undefined; + private _pageCountAlias: string | undefined; + private _pageCountMethod: "count" | "countAs" | undefined; + private _dynamicSearchWarnings: DynamicSearchWarning[] = []; + + constructor(minimal = false) { + this.query = new SelectQuery("Payment"); + this.filters.push({ version: { "$gte": 1 } }); + if (minimal) { + this.selectId(); + this.selectVersion(); + } + else this.selectSelfFields(); + } + + comment(c: string): this { + this.query.comment(c); + this._comment = c; + return this; + } + + filterByJson(source: unknown, warn?: (warning: DynamicSearchWarning) => void): this { + const base = this.query.clone(); + if (this.filters.length > 0) base.filter({ "$and": this.filters }); + const merged = mergeGeneratedScalarSearch(base, source, GENERATED_SEARCH_MODELS, warn); + this.query = merged.query as SelectQuery; + this.filters = this.query.filterCondition ? [this.query.filterCondition] : []; + this._dynamicSearchWarnings.push(...merged.warnings); + return this; + } + + get dynamicSearchWarnings(): readonly DynamicSearchWarning[] { + return this._dynamicSearchWarnings.map(warning => ({ ...warning })); + } + + purpose(p: string): ExecutablePaymentRequest { + this.query.purpose(p); + this._purpose = p; + return new ExecutablePaymentRequest( + (context) => context.withQueryRequest(this.query, () => this.executeForListInternal(context)), + (context) => context.withQueryRequest(this.query, () => this.executeForRowsInternal(context)), + (context, chunkSize) => this.executeForStreamInternal(context, chunkSize), + () => this.limit(1), + (c) => this.comment(c), + () => this.ensureIntent()); + } + + optimizeForContinuousPageFetch(): this { + this.query.optimizeForContinuousPageFetch(); + return this; + } + + optimizeForContinuousPageFetchWith(namespace: string, ttlSeconds: number): this { + this.query.optimizeForContinuousPageFetchWith(namespace, ttlSeconds); + return this; + } + + optimizePaginationWithIdSet(): this { + this.query.optimizePaginationWithIdSet(); + return this; + } + + optimizePaginationWithIdSetConfig(namespace: string, ttlSeconds: number, maxIds: number): this { + this.query.optimizePaginationWithIdSetConfig(namespace, ttlSeconds, maxIds); + return this; + } + + topNProbeParentThreshold(threshold: number): this { + this.query.topNProbeParentThreshold(threshold); + return this; + } + + limit(n: number): this { + this.query.limit(n); + return this; + } + + offset(n: number): this { + this.query.offset(n); + return this; + } + + toQuery(): SelectQuery { + if (this.filters.length > 0) { + this.query.filter({ "$and": this.filters }); + } + return this.query; + } + + withDeletedRows(): this { + this.filters = this.filters.filter(filter => !("version" in filter)); + return this; + } + + deletedRowsOnly(): this { + this.withDeletedRows(); + this.filters.push({ version: { "$lte": -1 } }); + return this; + } + + selectSelfFields(): this { + this.query.select(["id", "customerOrder", "referenceCode", "version"]); + return this; + } + + selectId(): this { + this.query.select(["id"]); + return this; + } + + + selectReferenceCode(): this { + this.query.select(["referenceCode"]); + return this; + } + + selectVersion(): this { + this.query.select(["version"]); + return this; + } + + + selectCustomerOrderWith(request: { toQuery(): SelectQuery }): this { + this.query.select(["customerOrder"]); + this.query.relationQuery( + "customerOrder", request.toQuery(), "customerOrder", "id", false, + { kind: "relation-select", method: "selectCustomerOrderWith", relation: "customerOrder" }); + return this; + } + + withCustomerOrderMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "customerOrder": { + "$inSubquery": { query: request.toQuery(), field: "id" }, + }, + }); + return this; + } + + withoutCustomerOrderMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "customerOrder": { + "$notInSubquery": { query: request.toQuery(), field: "id" }, + }, + }); + return this; + } + + selectPaymentAttemptListWith(request: { toQuery(): SelectQuery }): this { + this.query.relationQuery( + "paymentAttemptList", request.toQuery(), "id", "payment", true, + { kind: "relation-select", method: "selectPaymentAttemptListWith", relation: "paymentAttemptList" }); + return this; + } + + havePaymentAttempts(): this { + return this.withPaymentAttemptListMatching({ + toQuery: () => new SelectQuery("PaymentAttempt"), + }); + } + + haveNoPaymentAttempts(): this { + return this.withoutPaymentAttemptListMatching({ + toQuery: () => new SelectQuery("PaymentAttempt"), + }); + } + + withPaymentAttemptListMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "id": { + "$inSubquery": { + query: request.toQuery(), field: "payment", + }, + }, + }); + return this; + } + + withoutPaymentAttemptListMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "id": { + "$notInSubquery": { + query: request.toQuery(), field: "payment", + }, + }, + }); + return this; + } + + countPaymentAttempts(): this { + return this.countPaymentAttemptsAs("countPaymentAttempts"); + } + + countPaymentAttemptsAs(alias: string): this { + const query = new SelectQuery("PaymentAttempt").filter({ version: { "$gte": 1 } }); + this.countPaymentAttemptsWith(alias, { toQuery: () => query }); + this.query.relationAggregates[this.query.relationAggregates.length - 1].operation = { + kind: "relation-count", method: "countPaymentAttemptsAs", relation: "paymentAttemptList", alias }; + return this; + } + + countPaymentAttemptsWith(alias: string, child: { toQuery(): SelectQuery }): this { + const query = child.toQuery().clone(); + query.aggregateItems = []; + query.aggregate("Count", "id", alias); + this.query.relationAggregate("paymentAttemptList", alias, query, true, + { kind: "relation-count", method: "countPaymentAttemptsWith", relation: "paymentAttemptList", alias }); + return this; + } + + + + withIdIs(val: any): this { + this.filters.push({ "id": { "$eq": val } }); + return this; + } + + withIdIsNot(val: any): this { + this.filters.push({ "id": { "$ne": val } }); + return this; + } + + withIdIn(...vals: any[]): this { + this.filters.push({ "id": { "$in": vals } }); + return this; + } + + withIdNotIn(...vals: any[]): this { + this.filters.push({ "id": { "$notIn": vals } }); + return this; + } + + withIdGreaterThan(val: any): this { + this.filters.push({ "id": { "$gt": val } }); + return this; + } + + withIdGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "id": { "$gte": val } }); + return this; + } + + withIdLessThan(val: any): this { + this.filters.push({ "id": { "$lt": val } }); + return this; + } + + withIdLessThanOrEqualTo(val: any): this { + this.filters.push({ "id": { "$lte": val } }); + return this; + } + + withIdBetween(lower: any, upper: any): this { + this.filters.push({ "id": { "$between": [lower, upper] } }); + return this; + } + + withIdIsKnown(): this { + this.filters.push({ "id": { "$isNull": false } }); + return this; + } + + withIdIsUnknown(): this { + this.filters.push({ "id": { "$isNull": true } }); + return this; + } + + filterByCustomerOrder(val: any): this { + this.filters.push({ "customerOrder": { "$eq": val } }); + return this; + } + + filterByCustomerOrderIn(...vals: any[]): this { + this.filters.push({ "customerOrder": { "$in": vals } }); + return this; + } + + withCustomerOrderIsKnown(): this { + this.filters.push({ "customerOrder": { "$isNull": false } }); + return this; + } + + withCustomerOrderIsUnknown(): this { + this.filters.push({ "customerOrder": { "$isNull": true } }); + return this; + } + + withReferenceCodeContaining(val: string): this { + this.filters.push({ "referenceCode": { "$contains": val } }); + return this; + } + + withReferenceCodeIs(val: string): this { + this.filters.push({ "referenceCode": { "$eq": val } }); + return this; + } + + withReferenceCodeIsNot(val: any): this { + this.filters.push({ "referenceCode": { "$ne": val } }); + return this; + } + + withReferenceCodeIn(...vals: any[]): this { + this.filters.push({ "referenceCode": { "$in": vals } }); + return this; + } + + withReferenceCodeNotIn(...vals: any[]): this { + this.filters.push({ "referenceCode": { "$notIn": vals } }); + return this; + } + + withReferenceCodeGreaterThan(val: any): this { + this.filters.push({ "referenceCode": { "$gt": val } }); + return this; + } + + withReferenceCodeGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "referenceCode": { "$gte": val } }); + return this; + } + + withReferenceCodeLessThan(val: any): this { + this.filters.push({ "referenceCode": { "$lt": val } }); + return this; + } + + withReferenceCodeLessThanOrEqualTo(val: any): this { + this.filters.push({ "referenceCode": { "$lte": val } }); + return this; + } + + withReferenceCodeBetween(lower: any, upper: any): this { + this.filters.push({ "referenceCode": { "$between": [lower, upper] } }); + return this; + } + + withReferenceCodeIsKnown(): this { + this.filters.push({ "referenceCode": { "$isNull": false } }); + return this; + } + + withReferenceCodeIsUnknown(): this { + this.filters.push({ "referenceCode": { "$isNull": true } }); + return this; + } + withReferenceCodeNotContaining(val: string): this { + this.filters.push({ "referenceCode": { "$notContains": val } }); + return this; + } + + withReferenceCodeStartingWith(val: string): this { + this.filters.push({ "referenceCode": { "$startsWith": val } }); + return this; + } + + withReferenceCodeNotStartingWith(val: string): this { + this.filters.push({ "referenceCode": { "$notStartsWith": val } }); + return this; + } + + withReferenceCodeEndingWith(val: string): this { + this.filters.push({ "referenceCode": { "$endsWith": val } }); + return this; + } + + withReferenceCodeNotEndingWith(val: string): this { + this.filters.push({ "referenceCode": { "$notEndsWith": val } }); + return this; + } + + withReferenceCodeSoundingLike(val: string): this { + this.filters.push({ "referenceCode": { "$soundLike": val } }); + return this; + } + + withVersionIs(val: any): this { + this.filters.push({ "version": { "$eq": val } }); + return this; + } + + withVersionIsNot(val: any): this { + this.filters.push({ "version": { "$ne": val } }); + return this; + } + + withVersionIn(...vals: any[]): this { + this.filters.push({ "version": { "$in": vals } }); + return this; + } + + withVersionNotIn(...vals: any[]): this { + this.filters.push({ "version": { "$notIn": vals } }); + return this; + } + + withVersionGreaterThan(val: any): this { + this.filters.push({ "version": { "$gt": val } }); + return this; + } + + withVersionGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "version": { "$gte": val } }); + return this; + } + + withVersionLessThan(val: any): this { + this.filters.push({ "version": { "$lt": val } }); + return this; + } + + withVersionLessThanOrEqualTo(val: any): this { + this.filters.push({ "version": { "$lte": val } }); + return this; + } + + withVersionBetween(lower: any, upper: any): this { + this.filters.push({ "version": { "$between": [lower, upper] } }); + return this; + } + + withVersionIsKnown(): this { + this.filters.push({ "version": { "$isNull": false } }); + return this; + } + + withVersionIsUnknown(): this { + this.filters.push({ "version": { "$isNull": true } }); + return this; + } + + orderByIdAscending(): this { + this.query.orderBy("id", "asc"); + return this; + } + + orderByIdDescending(): this { + this.query.orderBy("id", "desc"); + return this; + } + + + orderByReferenceCodeAscending(): this { + this.query.orderBy("referenceCode", "asc"); + return this; + } + + orderByReferenceCodeDescending(): this { + this.query.orderBy("referenceCode", "desc"); + return this; + } + + orderByVersionAscending(): this { + this.query.orderBy("version", "asc"); + return this; + } + + orderByVersionDescending(): this { + this.query.orderBy("version", "desc"); + return this; + } + + + // --- Aggregations --- + count(): this { + this.query.aggregate("Count", "id", "count"); + this._pageCountAlias = "count"; + this._pageCountMethod = "count"; + return this; + } + + countAs(retName: string): this { + this.query.aggregate("Count", "id", retName); + this._pageCountAlias = retName; + this._pageCountMethod = "countAs"; + return this; + } + + + // --- Group By --- + groupById(): this { + this.query.groupBy("id"); + return this; + } + + groupByIdAs(retName: string): this { + this.query.groupBy("id"); // In TS we don't alias group by yet natively + return this; + } + groupByCustomerOrder(): this { + this.query.groupBy("customerOrder"); + return this; + } + + groupByCustomerOrderAs(retName: string): this { + this.query.groupBy("customerOrder"); // In TS we don't alias group by yet natively + return this; + } + groupByReferenceCode(): this { + this.query.groupBy("referenceCode"); + return this; + } + + groupByReferenceCodeAs(retName: string): this { + this.query.groupBy("referenceCode"); // In TS we don't alias group by yet natively + return this; + } + groupByVersion(): this { + this.query.groupBy("version"); + return this; + } + + groupByVersionAs(retName: string): this { + this.query.groupBy("version"); // In TS we don't alias group by yet natively + return this; + } + + // --- Facets --- + facetByCustomerOrderAs( + facetName: string, + request: { toQuery(): SelectQuery }, + includeAllFacets = true, + ): this { + this.query.facetBy(facetName, "customerOrder", request, includeAllFacets, + { kind: "facet", method: "facetByCustomerOrderAs", relation: "customerOrder", alias: facetName }); + return this; + } + + + private async executeForListInternal(context: UserContext): Promise> { + this.ensureIntent(); + if (this._pageCountAlias !== undefined) return this.executeCountedListInternal(context); + if (this.filters.length > 0) { + this.query.filter({ "$and": this.filters }); + } + + const service = context.requireResource("dataService"); + const rows = await service.executeQuery(context.prepareQuery(this.query)); + const aggregateOnly = this.query.aggregateItems.length > 0 && this.query.groupByItems.length === 0; + const data = aggregateOnly ? [] : rows.map((row: unknown) => + row instanceof Payment + ? row + : Payment.fromRecord(row as Record, new EntityRoot())); + const result = new SmartList(data); + if (aggregateOnly && rows[0]) Object.assign(result.aggregations, rows[0]); + + if (this.query.facets.length > 0) { + result.facets = await executeRelationFacets( + service, (query: any) => context.prepareQuery(query), this.query, this.query.facets); + } + + return result; + } + + private async executeForRowsInternal(context: UserContext): Promise>> { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + return new SmartList>( + await service.executeQuery(context.prepareQuery(this.query)) as Record[]); + } + + private async executeCountedListInternal( + context: UserContext, + ): Promise> { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + const preparedQuery = context.prepareQuery(this.query); + const useIdSet = preparedQuery.localIdSetPaginationOptions() !== undefined; + const countOperation = { kind: "named-count" as const, + method: this._pageCountMethod ?? "count", + ...(this._pageCountMethod === "countAs" ? { alias: this._pageCountAlias } : {}) }; + const totalCountBeforeRows = useIdSet ? undefined : await service.executeCount( + preparedQuery, this._pageCountAlias, countOperation); + const pageQuery = preparedQuery.clone(); + // A page count describes the full filtered set, not an aggregate-only page row. + pageQuery.aggregateItems = pageQuery.aggregateItems.filter(item => + !(String(item.function).toLowerCase() === "count" && item.alias === this._pageCountAlias)); + const rows = await service.executeQuery(pageQuery); + const totalCount = useIdSet && context.idSetPaginationCountAccuracy === "EXACT" + ? context.idSetPaginationCount! + : (totalCountBeforeRows ?? await service.executeCount(preparedQuery, this._pageCountAlias, countOperation)); + const data = new SmartList(rows.map((row: unknown) => + row instanceof Payment + ? row + : Payment.fromRecord(row as Record, new EntityRoot()))); + data.totalCount = totalCount; + if (this._pageCountAlias !== undefined) data.aggregations[this._pageCountAlias] = totalCount; + if (this.query.facets.length > 0) { + data.facets = await executeRelationFacets( + service, (query: any) => context.prepareQuery(query), preparedQuery, preparedQuery.facets); + } + return data; + } + + private executeForStreamInternal(context: UserContext, chunkSize: number): AsyncIterable { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + const preparedQuery = context.prepareQuery(this.query); + // Prepare Context and capture the runtime request before returning a lazy iterator. + const chunks = service.executeForStream(preparedQuery, chunkSize); + return (async function* () { + for await (const chunk of chunks) { + for (const entity of chunk) { + yield entity instanceof Payment + ? entity + : Payment.fromRecord(entity as Record, new EntityRoot()); + } + } + })(); + } + + private ensureIntent(): void { + // Runtime owns validation, Unicode whitespace rules and typed diagnostics. + new QueryIntent(this._comment, this._purpose); + } + +} + +export class ExecutablePaymentRequest { + constructor( + private readonly execute: (context: UserContext) => Promise>, + private readonly executeRows: (context: UserContext) => Promise>>, + private readonly stream: (context: UserContext, chunkSize: number) => AsyncIterable, + private readonly limitOne: () => void, + private readonly addComment: (comment: string) => void, + private readonly ensureIntent: () => void, + ) {} + + comment(c: string): this { + this.addComment(c); + return this; + } + + newEntity(context: UserContext): Payment { + this.ensureIntent(); + return new Payment(); + } + + executeForList(context: UserContext): Promise> { + this.ensureIntent(); + return this.execute(context); + } + + executeForRows(context: UserContext): Promise>> { + this.ensureIntent(); + return this.executeRows(context); + } + + executeForStream(context: UserContext, chunkSize = 1000): AsyncIterable { + this.ensureIntent(); + return this.stream(context, chunkSize); + } + + async executeForOne(context: UserContext): Promise { + this.limitOne(); + return (await this.executeForList(context))[0]; + } +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/PlatformRequest.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/PlatformRequest.ts new file mode 100644 index 0000000000000000000000000000000000000000..c88ef6a0bf86cd308da8f3bfaf18d4e65f936462 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/PlatformRequest.ts @@ -0,0 +1,607 @@ +import { EntityRoot, SelectQuery, SmartList, TeaQLDataService, UserContext, executeRelationFacets } from '../../teaql-ts'; +import { Platform } from '../models/Platform'; +import { mergeGeneratedScalarSearch, DynamicSearchWarning, QueryIntent } from 'teaql-ts'; +import { GENERATED_SEARCH_MODELS } from '../../generated-search-models'; + + + +export class PlatformRequest { + private query: SelectQuery; + private filters: any[] = []; + private _purpose: string | undefined; + private _comment: string | undefined; + private _pageCountAlias: string | undefined; + private _pageCountMethod: "count" | "countAs" | undefined; + private _dynamicSearchWarnings: DynamicSearchWarning[] = []; + + constructor(minimal = false) { + this.query = new SelectQuery("Platform"); + this.filters.push({ version: { "$gte": 1 } }); + if (minimal) { + this.selectId(); + this.selectVersion(); + } + else this.selectSelfFields(); + } + + comment(c: string): this { + this.query.comment(c); + this._comment = c; + return this; + } + + filterByJson(source: unknown, warn?: (warning: DynamicSearchWarning) => void): this { + const base = this.query.clone(); + if (this.filters.length > 0) base.filter({ "$and": this.filters }); + const merged = mergeGeneratedScalarSearch(base, source, GENERATED_SEARCH_MODELS, warn); + this.query = merged.query as SelectQuery; + this.filters = this.query.filterCondition ? [this.query.filterCondition] : []; + this._dynamicSearchWarnings.push(...merged.warnings); + return this; + } + + get dynamicSearchWarnings(): readonly DynamicSearchWarning[] { + return this._dynamicSearchWarnings.map(warning => ({ ...warning })); + } + + purpose(p: string): ExecutablePlatformRequest { + this.query.purpose(p); + this._purpose = p; + return new ExecutablePlatformRequest( + (context) => context.withQueryRequest(this.query, () => this.executeForListInternal(context)), + (context) => context.withQueryRequest(this.query, () => this.executeForRowsInternal(context)), + (context, chunkSize) => this.executeForStreamInternal(context, chunkSize), + () => this.limit(1), + (c) => this.comment(c), + () => this.ensureIntent()); + } + + optimizeForContinuousPageFetch(): this { + this.query.optimizeForContinuousPageFetch(); + return this; + } + + optimizeForContinuousPageFetchWith(namespace: string, ttlSeconds: number): this { + this.query.optimizeForContinuousPageFetchWith(namespace, ttlSeconds); + return this; + } + + optimizePaginationWithIdSet(): this { + this.query.optimizePaginationWithIdSet(); + return this; + } + + optimizePaginationWithIdSetConfig(namespace: string, ttlSeconds: number, maxIds: number): this { + this.query.optimizePaginationWithIdSetConfig(namespace, ttlSeconds, maxIds); + return this; + } + + topNProbeParentThreshold(threshold: number): this { + this.query.topNProbeParentThreshold(threshold); + return this; + } + + limit(n: number): this { + this.query.limit(n); + return this; + } + + offset(n: number): this { + this.query.offset(n); + return this; + } + + toQuery(): SelectQuery { + if (this.filters.length > 0) { + this.query.filter({ "$and": this.filters }); + } + return this.query; + } + + withDeletedRows(): this { + this.filters = this.filters.filter(filter => !("version" in filter)); + return this; + } + + deletedRowsOnly(): this { + this.withDeletedRows(); + this.filters.push({ version: { "$lte": -1 } }); + return this; + } + + selectSelfFields(): this { + this.query.select(["id", "name", "version"]); + return this; + } + + selectId(): this { + this.query.select(["id"]); + return this; + } + + selectName(): this { + this.query.select(["name"]); + return this; + } + + selectVersion(): this { + this.query.select(["version"]); + return this; + } + + + + + selectCustomerOrderListWith(request: { toQuery(): SelectQuery }): this { + this.query.relationQuery( + "customerOrderList", request.toQuery(), "id", "platform", true, + { kind: "relation-select", method: "selectCustomerOrderListWith", relation: "customerOrderList" }); + return this; + } + + haveCustomerOrders(): this { + return this.withCustomerOrderListMatching({ + toQuery: () => new SelectQuery("CustomerOrder"), + }); + } + + haveNoCustomerOrders(): this { + return this.withoutCustomerOrderListMatching({ + toQuery: () => new SelectQuery("CustomerOrder"), + }); + } + + withCustomerOrderListMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "id": { + "$inSubquery": { + query: request.toQuery(), field: "platform", + }, + }, + }); + return this; + } + + withoutCustomerOrderListMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "id": { + "$notInSubquery": { + query: request.toQuery(), field: "platform", + }, + }, + }); + return this; + } + + countCustomerOrders(): this { + return this.countCustomerOrdersAs("countCustomerOrders"); + } + + countCustomerOrdersAs(alias: string): this { + const query = new SelectQuery("CustomerOrder").filter({ version: { "$gte": 1 } }); + this.countCustomerOrdersWith(alias, { toQuery: () => query }); + this.query.relationAggregates[this.query.relationAggregates.length - 1].operation = { + kind: "relation-count", method: "countCustomerOrdersAs", relation: "customerOrderList", alias }; + return this; + } + + countCustomerOrdersWith(alias: string, child: { toQuery(): SelectQuery }): this { + const query = child.toQuery().clone(); + query.aggregateItems = []; + query.aggregate("Count", "id", alias); + this.query.relationAggregate("customerOrderList", alias, query, true, + { kind: "relation-count", method: "countCustomerOrdersWith", relation: "customerOrderList", alias }); + return this; + } + + + + withIdIs(val: any): this { + this.filters.push({ "id": { "$eq": val } }); + return this; + } + + withIdIsNot(val: any): this { + this.filters.push({ "id": { "$ne": val } }); + return this; + } + + withIdIn(...vals: any[]): this { + this.filters.push({ "id": { "$in": vals } }); + return this; + } + + withIdNotIn(...vals: any[]): this { + this.filters.push({ "id": { "$notIn": vals } }); + return this; + } + + withIdGreaterThan(val: any): this { + this.filters.push({ "id": { "$gt": val } }); + return this; + } + + withIdGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "id": { "$gte": val } }); + return this; + } + + withIdLessThan(val: any): this { + this.filters.push({ "id": { "$lt": val } }); + return this; + } + + withIdLessThanOrEqualTo(val: any): this { + this.filters.push({ "id": { "$lte": val } }); + return this; + } + + withIdBetween(lower: any, upper: any): this { + this.filters.push({ "id": { "$between": [lower, upper] } }); + return this; + } + + withIdIsKnown(): this { + this.filters.push({ "id": { "$isNull": false } }); + return this; + } + + withIdIsUnknown(): this { + this.filters.push({ "id": { "$isNull": true } }); + return this; + } + + withNameContaining(val: string): this { + this.filters.push({ "name": { "$contains": val } }); + return this; + } + + withNameIs(val: string): this { + this.filters.push({ "name": { "$eq": val } }); + return this; + } + + withNameIsNot(val: any): this { + this.filters.push({ "name": { "$ne": val } }); + return this; + } + + withNameIn(...vals: any[]): this { + this.filters.push({ "name": { "$in": vals } }); + return this; + } + + withNameNotIn(...vals: any[]): this { + this.filters.push({ "name": { "$notIn": vals } }); + return this; + } + + withNameGreaterThan(val: any): this { + this.filters.push({ "name": { "$gt": val } }); + return this; + } + + withNameGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "name": { "$gte": val } }); + return this; + } + + withNameLessThan(val: any): this { + this.filters.push({ "name": { "$lt": val } }); + return this; + } + + withNameLessThanOrEqualTo(val: any): this { + this.filters.push({ "name": { "$lte": val } }); + return this; + } + + withNameBetween(lower: any, upper: any): this { + this.filters.push({ "name": { "$between": [lower, upper] } }); + return this; + } + + withNameIsKnown(): this { + this.filters.push({ "name": { "$isNull": false } }); + return this; + } + + withNameIsUnknown(): this { + this.filters.push({ "name": { "$isNull": true } }); + return this; + } + withNameNotContaining(val: string): this { + this.filters.push({ "name": { "$notContains": val } }); + return this; + } + + withNameStartingWith(val: string): this { + this.filters.push({ "name": { "$startsWith": val } }); + return this; + } + + withNameNotStartingWith(val: string): this { + this.filters.push({ "name": { "$notStartsWith": val } }); + return this; + } + + withNameEndingWith(val: string): this { + this.filters.push({ "name": { "$endsWith": val } }); + return this; + } + + withNameNotEndingWith(val: string): this { + this.filters.push({ "name": { "$notEndsWith": val } }); + return this; + } + + withNameSoundingLike(val: string): this { + this.filters.push({ "name": { "$soundLike": val } }); + return this; + } + + withVersionIs(val: any): this { + this.filters.push({ "version": { "$eq": val } }); + return this; + } + + withVersionIsNot(val: any): this { + this.filters.push({ "version": { "$ne": val } }); + return this; + } + + withVersionIn(...vals: any[]): this { + this.filters.push({ "version": { "$in": vals } }); + return this; + } + + withVersionNotIn(...vals: any[]): this { + this.filters.push({ "version": { "$notIn": vals } }); + return this; + } + + withVersionGreaterThan(val: any): this { + this.filters.push({ "version": { "$gt": val } }); + return this; + } + + withVersionGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "version": { "$gte": val } }); + return this; + } + + withVersionLessThan(val: any): this { + this.filters.push({ "version": { "$lt": val } }); + return this; + } + + withVersionLessThanOrEqualTo(val: any): this { + this.filters.push({ "version": { "$lte": val } }); + return this; + } + + withVersionBetween(lower: any, upper: any): this { + this.filters.push({ "version": { "$between": [lower, upper] } }); + return this; + } + + withVersionIsKnown(): this { + this.filters.push({ "version": { "$isNull": false } }); + return this; + } + + withVersionIsUnknown(): this { + this.filters.push({ "version": { "$isNull": true } }); + return this; + } + + orderByIdAscending(): this { + this.query.orderBy("id", "asc"); + return this; + } + + orderByIdDescending(): this { + this.query.orderBy("id", "desc"); + return this; + } + + orderByNameAscending(): this { + this.query.orderBy("name", "asc"); + return this; + } + + orderByNameDescending(): this { + this.query.orderBy("name", "desc"); + return this; + } + + orderByVersionAscending(): this { + this.query.orderBy("version", "asc"); + return this; + } + + orderByVersionDescending(): this { + this.query.orderBy("version", "desc"); + return this; + } + + + // --- Aggregations --- + count(): this { + this.query.aggregate("Count", "id", "count"); + this._pageCountAlias = "count"; + this._pageCountMethod = "count"; + return this; + } + + countAs(retName: string): this { + this.query.aggregate("Count", "id", retName); + this._pageCountAlias = retName; + this._pageCountMethod = "countAs"; + return this; + } + + + // --- Group By --- + groupById(): this { + this.query.groupBy("id"); + return this; + } + + groupByIdAs(retName: string): this { + this.query.groupBy("id"); // In TS we don't alias group by yet natively + return this; + } + groupByName(): this { + this.query.groupBy("name"); + return this; + } + + groupByNameAs(retName: string): this { + this.query.groupBy("name"); // In TS we don't alias group by yet natively + return this; + } + groupByVersion(): this { + this.query.groupBy("version"); + return this; + } + + groupByVersionAs(retName: string): this { + this.query.groupBy("version"); // In TS we don't alias group by yet natively + return this; + } + + // --- Facets --- + + private async executeForListInternal(context: UserContext): Promise> { + this.ensureIntent(); + if (this._pageCountAlias !== undefined) return this.executeCountedListInternal(context); + if (this.filters.length > 0) { + this.query.filter({ "$and": this.filters }); + } + + const service = context.requireResource("dataService"); + const rows = await service.executeQuery(context.prepareQuery(this.query)); + const aggregateOnly = this.query.aggregateItems.length > 0 && this.query.groupByItems.length === 0; + const data = aggregateOnly ? [] : rows.map((row: unknown) => + row instanceof Platform + ? row + : Platform.fromRecord(row as Record, new EntityRoot())); + const result = new SmartList(data); + if (aggregateOnly && rows[0]) Object.assign(result.aggregations, rows[0]); + + if (this.query.facets.length > 0) { + result.facets = await executeRelationFacets( + service, (query: any) => context.prepareQuery(query), this.query, this.query.facets); + } + + return result; + } + + private async executeForRowsInternal(context: UserContext): Promise>> { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + return new SmartList>( + await service.executeQuery(context.prepareQuery(this.query)) as Record[]); + } + + private async executeCountedListInternal( + context: UserContext, + ): Promise> { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + const preparedQuery = context.prepareQuery(this.query); + const useIdSet = preparedQuery.localIdSetPaginationOptions() !== undefined; + const countOperation = { kind: "named-count" as const, + method: this._pageCountMethod ?? "count", + ...(this._pageCountMethod === "countAs" ? { alias: this._pageCountAlias } : {}) }; + const totalCountBeforeRows = useIdSet ? undefined : await service.executeCount( + preparedQuery, this._pageCountAlias, countOperation); + const pageQuery = preparedQuery.clone(); + // A page count describes the full filtered set, not an aggregate-only page row. + pageQuery.aggregateItems = pageQuery.aggregateItems.filter(item => + !(String(item.function).toLowerCase() === "count" && item.alias === this._pageCountAlias)); + const rows = await service.executeQuery(pageQuery); + const totalCount = useIdSet && context.idSetPaginationCountAccuracy === "EXACT" + ? context.idSetPaginationCount! + : (totalCountBeforeRows ?? await service.executeCount(preparedQuery, this._pageCountAlias, countOperation)); + const data = new SmartList(rows.map((row: unknown) => + row instanceof Platform + ? row + : Platform.fromRecord(row as Record, new EntityRoot()))); + data.totalCount = totalCount; + if (this._pageCountAlias !== undefined) data.aggregations[this._pageCountAlias] = totalCount; + if (this.query.facets.length > 0) { + data.facets = await executeRelationFacets( + service, (query: any) => context.prepareQuery(query), preparedQuery, preparedQuery.facets); + } + return data; + } + + private executeForStreamInternal(context: UserContext, chunkSize: number): AsyncIterable { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + const preparedQuery = context.prepareQuery(this.query); + // Prepare Context and capture the runtime request before returning a lazy iterator. + const chunks = service.executeForStream(preparedQuery, chunkSize); + return (async function* () { + for await (const chunk of chunks) { + for (const entity of chunk) { + yield entity instanceof Platform + ? entity + : Platform.fromRecord(entity as Record, new EntityRoot()); + } + } + })(); + } + + private ensureIntent(): void { + // Runtime owns validation, Unicode whitespace rules and typed diagnostics. + new QueryIntent(this._comment, this._purpose); + } + +} + +export class ExecutablePlatformRequest { + constructor( + private readonly execute: (context: UserContext) => Promise>, + private readonly executeRows: (context: UserContext) => Promise>>, + private readonly stream: (context: UserContext, chunkSize: number) => AsyncIterable, + private readonly limitOne: () => void, + private readonly addComment: (comment: string) => void, + private readonly ensureIntent: () => void, + ) {} + + comment(c: string): this { + this.addComment(c); + return this; + } + + newEntity(context: UserContext): Platform { + this.ensureIntent(); + return new Platform(); + } + + executeForList(context: UserContext): Promise> { + this.ensureIntent(); + return this.execute(context); + } + + executeForRows(context: UserContext): Promise>> { + this.ensureIntent(); + return this.executeRows(context); + } + + executeForStream(context: UserContext, chunkSize = 1000): AsyncIterable { + this.ensureIntent(); + return this.stream(context, chunkSize); + } + + async executeForOne(context: UserContext): Promise { + this.limitOne(); + return (await this.executeForList(context))[0]; + } +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/ShipmentRequest.ts b/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/ShipmentRequest.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9e044613521879a2f627df51735bc8fabd3348c --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/generated/requests/ShipmentRequest.ts @@ -0,0 +1,611 @@ +import { EntityRoot, SelectQuery, SmartList, TeaQLDataService, UserContext, executeRelationFacets } from '../../teaql-ts'; +import { Shipment } from '../models/Shipment'; +import { mergeGeneratedScalarSearch, DynamicSearchWarning, QueryIntent } from 'teaql-ts'; +import { GENERATED_SEARCH_MODELS } from '../../generated-search-models'; + + + +export class ShipmentRequest { + private query: SelectQuery; + private filters: any[] = []; + private _purpose: string | undefined; + private _comment: string | undefined; + private _pageCountAlias: string | undefined; + private _pageCountMethod: "count" | "countAs" | undefined; + private _dynamicSearchWarnings: DynamicSearchWarning[] = []; + + constructor(minimal = false) { + this.query = new SelectQuery("Shipment"); + this.filters.push({ version: { "$gte": 1 } }); + if (minimal) { + this.selectId(); + this.selectVersion(); + } + else this.selectSelfFields(); + } + + comment(c: string): this { + this.query.comment(c); + this._comment = c; + return this; + } + + filterByJson(source: unknown, warn?: (warning: DynamicSearchWarning) => void): this { + const base = this.query.clone(); + if (this.filters.length > 0) base.filter({ "$and": this.filters }); + const merged = mergeGeneratedScalarSearch(base, source, GENERATED_SEARCH_MODELS, warn); + this.query = merged.query as SelectQuery; + this.filters = this.query.filterCondition ? [this.query.filterCondition] : []; + this._dynamicSearchWarnings.push(...merged.warnings); + return this; + } + + get dynamicSearchWarnings(): readonly DynamicSearchWarning[] { + return this._dynamicSearchWarnings.map(warning => ({ ...warning })); + } + + purpose(p: string): ExecutableShipmentRequest { + this.query.purpose(p); + this._purpose = p; + return new ExecutableShipmentRequest( + (context) => context.withQueryRequest(this.query, () => this.executeForListInternal(context)), + (context) => context.withQueryRequest(this.query, () => this.executeForRowsInternal(context)), + (context, chunkSize) => this.executeForStreamInternal(context, chunkSize), + () => this.limit(1), + (c) => this.comment(c), + () => this.ensureIntent()); + } + + optimizeForContinuousPageFetch(): this { + this.query.optimizeForContinuousPageFetch(); + return this; + } + + optimizeForContinuousPageFetchWith(namespace: string, ttlSeconds: number): this { + this.query.optimizeForContinuousPageFetchWith(namespace, ttlSeconds); + return this; + } + + optimizePaginationWithIdSet(): this { + this.query.optimizePaginationWithIdSet(); + return this; + } + + optimizePaginationWithIdSetConfig(namespace: string, ttlSeconds: number, maxIds: number): this { + this.query.optimizePaginationWithIdSetConfig(namespace, ttlSeconds, maxIds); + return this; + } + + topNProbeParentThreshold(threshold: number): this { + this.query.topNProbeParentThreshold(threshold); + return this; + } + + limit(n: number): this { + this.query.limit(n); + return this; + } + + offset(n: number): this { + this.query.offset(n); + return this; + } + + toQuery(): SelectQuery { + if (this.filters.length > 0) { + this.query.filter({ "$and": this.filters }); + } + return this.query; + } + + withDeletedRows(): this { + this.filters = this.filters.filter(filter => !("version" in filter)); + return this; + } + + deletedRowsOnly(): this { + this.withDeletedRows(); + this.filters.push({ version: { "$lte": -1 } }); + return this; + } + + selectSelfFields(): this { + this.query.select(["id", "customerOrder", "referenceCode", "version"]); + return this; + } + + selectId(): this { + this.query.select(["id"]); + return this; + } + + + selectReferenceCode(): this { + this.query.select(["referenceCode"]); + return this; + } + + selectVersion(): this { + this.query.select(["version"]); + return this; + } + + + selectCustomerOrderWith(request: { toQuery(): SelectQuery }): this { + this.query.select(["customerOrder"]); + this.query.relationQuery( + "customerOrder", request.toQuery(), "customerOrder", "id", false, + { kind: "relation-select", method: "selectCustomerOrderWith", relation: "customerOrder" }); + return this; + } + + withCustomerOrderMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "customerOrder": { + "$inSubquery": { query: request.toQuery(), field: "id" }, + }, + }); + return this; + } + + withoutCustomerOrderMatching(request: { toQuery(): SelectQuery }): this { + this.filters.push({ + "customerOrder": { + "$notInSubquery": { query: request.toQuery(), field: "id" }, + }, + }); + return this; + } + + + + + withIdIs(val: any): this { + this.filters.push({ "id": { "$eq": val } }); + return this; + } + + withIdIsNot(val: any): this { + this.filters.push({ "id": { "$ne": val } }); + return this; + } + + withIdIn(...vals: any[]): this { + this.filters.push({ "id": { "$in": vals } }); + return this; + } + + withIdNotIn(...vals: any[]): this { + this.filters.push({ "id": { "$notIn": vals } }); + return this; + } + + withIdGreaterThan(val: any): this { + this.filters.push({ "id": { "$gt": val } }); + return this; + } + + withIdGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "id": { "$gte": val } }); + return this; + } + + withIdLessThan(val: any): this { + this.filters.push({ "id": { "$lt": val } }); + return this; + } + + withIdLessThanOrEqualTo(val: any): this { + this.filters.push({ "id": { "$lte": val } }); + return this; + } + + withIdBetween(lower: any, upper: any): this { + this.filters.push({ "id": { "$between": [lower, upper] } }); + return this; + } + + withIdIsKnown(): this { + this.filters.push({ "id": { "$isNull": false } }); + return this; + } + + withIdIsUnknown(): this { + this.filters.push({ "id": { "$isNull": true } }); + return this; + } + + filterByCustomerOrder(val: any): this { + this.filters.push({ "customerOrder": { "$eq": val } }); + return this; + } + + filterByCustomerOrderIn(...vals: any[]): this { + this.filters.push({ "customerOrder": { "$in": vals } }); + return this; + } + + withCustomerOrderIsKnown(): this { + this.filters.push({ "customerOrder": { "$isNull": false } }); + return this; + } + + withCustomerOrderIsUnknown(): this { + this.filters.push({ "customerOrder": { "$isNull": true } }); + return this; + } + + withReferenceCodeContaining(val: string): this { + this.filters.push({ "referenceCode": { "$contains": val } }); + return this; + } + + withReferenceCodeIs(val: string): this { + this.filters.push({ "referenceCode": { "$eq": val } }); + return this; + } + + withReferenceCodeIsNot(val: any): this { + this.filters.push({ "referenceCode": { "$ne": val } }); + return this; + } + + withReferenceCodeIn(...vals: any[]): this { + this.filters.push({ "referenceCode": { "$in": vals } }); + return this; + } + + withReferenceCodeNotIn(...vals: any[]): this { + this.filters.push({ "referenceCode": { "$notIn": vals } }); + return this; + } + + withReferenceCodeGreaterThan(val: any): this { + this.filters.push({ "referenceCode": { "$gt": val } }); + return this; + } + + withReferenceCodeGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "referenceCode": { "$gte": val } }); + return this; + } + + withReferenceCodeLessThan(val: any): this { + this.filters.push({ "referenceCode": { "$lt": val } }); + return this; + } + + withReferenceCodeLessThanOrEqualTo(val: any): this { + this.filters.push({ "referenceCode": { "$lte": val } }); + return this; + } + + withReferenceCodeBetween(lower: any, upper: any): this { + this.filters.push({ "referenceCode": { "$between": [lower, upper] } }); + return this; + } + + withReferenceCodeIsKnown(): this { + this.filters.push({ "referenceCode": { "$isNull": false } }); + return this; + } + + withReferenceCodeIsUnknown(): this { + this.filters.push({ "referenceCode": { "$isNull": true } }); + return this; + } + withReferenceCodeNotContaining(val: string): this { + this.filters.push({ "referenceCode": { "$notContains": val } }); + return this; + } + + withReferenceCodeStartingWith(val: string): this { + this.filters.push({ "referenceCode": { "$startsWith": val } }); + return this; + } + + withReferenceCodeNotStartingWith(val: string): this { + this.filters.push({ "referenceCode": { "$notStartsWith": val } }); + return this; + } + + withReferenceCodeEndingWith(val: string): this { + this.filters.push({ "referenceCode": { "$endsWith": val } }); + return this; + } + + withReferenceCodeNotEndingWith(val: string): this { + this.filters.push({ "referenceCode": { "$notEndsWith": val } }); + return this; + } + + withReferenceCodeSoundingLike(val: string): this { + this.filters.push({ "referenceCode": { "$soundLike": val } }); + return this; + } + + withVersionIs(val: any): this { + this.filters.push({ "version": { "$eq": val } }); + return this; + } + + withVersionIsNot(val: any): this { + this.filters.push({ "version": { "$ne": val } }); + return this; + } + + withVersionIn(...vals: any[]): this { + this.filters.push({ "version": { "$in": vals } }); + return this; + } + + withVersionNotIn(...vals: any[]): this { + this.filters.push({ "version": { "$notIn": vals } }); + return this; + } + + withVersionGreaterThan(val: any): this { + this.filters.push({ "version": { "$gt": val } }); + return this; + } + + withVersionGreaterThanOrEqualTo(val: any): this { + this.filters.push({ "version": { "$gte": val } }); + return this; + } + + withVersionLessThan(val: any): this { + this.filters.push({ "version": { "$lt": val } }); + return this; + } + + withVersionLessThanOrEqualTo(val: any): this { + this.filters.push({ "version": { "$lte": val } }); + return this; + } + + withVersionBetween(lower: any, upper: any): this { + this.filters.push({ "version": { "$between": [lower, upper] } }); + return this; + } + + withVersionIsKnown(): this { + this.filters.push({ "version": { "$isNull": false } }); + return this; + } + + withVersionIsUnknown(): this { + this.filters.push({ "version": { "$isNull": true } }); + return this; + } + + orderByIdAscending(): this { + this.query.orderBy("id", "asc"); + return this; + } + + orderByIdDescending(): this { + this.query.orderBy("id", "desc"); + return this; + } + + + orderByReferenceCodeAscending(): this { + this.query.orderBy("referenceCode", "asc"); + return this; + } + + orderByReferenceCodeDescending(): this { + this.query.orderBy("referenceCode", "desc"); + return this; + } + + orderByVersionAscending(): this { + this.query.orderBy("version", "asc"); + return this; + } + + orderByVersionDescending(): this { + this.query.orderBy("version", "desc"); + return this; + } + + + // --- Aggregations --- + count(): this { + this.query.aggregate("Count", "id", "count"); + this._pageCountAlias = "count"; + this._pageCountMethod = "count"; + return this; + } + + countAs(retName: string): this { + this.query.aggregate("Count", "id", retName); + this._pageCountAlias = retName; + this._pageCountMethod = "countAs"; + return this; + } + + + // --- Group By --- + groupById(): this { + this.query.groupBy("id"); + return this; + } + + groupByIdAs(retName: string): this { + this.query.groupBy("id"); // In TS we don't alias group by yet natively + return this; + } + groupByCustomerOrder(): this { + this.query.groupBy("customerOrder"); + return this; + } + + groupByCustomerOrderAs(retName: string): this { + this.query.groupBy("customerOrder"); // In TS we don't alias group by yet natively + return this; + } + groupByReferenceCode(): this { + this.query.groupBy("referenceCode"); + return this; + } + + groupByReferenceCodeAs(retName: string): this { + this.query.groupBy("referenceCode"); // In TS we don't alias group by yet natively + return this; + } + groupByVersion(): this { + this.query.groupBy("version"); + return this; + } + + groupByVersionAs(retName: string): this { + this.query.groupBy("version"); // In TS we don't alias group by yet natively + return this; + } + + // --- Facets --- + facetByCustomerOrderAs( + facetName: string, + request: { toQuery(): SelectQuery }, + includeAllFacets = true, + ): this { + this.query.facetBy(facetName, "customerOrder", request, includeAllFacets, + { kind: "facet", method: "facetByCustomerOrderAs", relation: "customerOrder", alias: facetName }); + return this; + } + + + private async executeForListInternal(context: UserContext): Promise> { + this.ensureIntent(); + if (this._pageCountAlias !== undefined) return this.executeCountedListInternal(context); + if (this.filters.length > 0) { + this.query.filter({ "$and": this.filters }); + } + + const service = context.requireResource("dataService"); + const rows = await service.executeQuery(context.prepareQuery(this.query)); + const aggregateOnly = this.query.aggregateItems.length > 0 && this.query.groupByItems.length === 0; + const data = aggregateOnly ? [] : rows.map((row: unknown) => + row instanceof Shipment + ? row + : Shipment.fromRecord(row as Record, new EntityRoot())); + const result = new SmartList(data); + if (aggregateOnly && rows[0]) Object.assign(result.aggregations, rows[0]); + + if (this.query.facets.length > 0) { + result.facets = await executeRelationFacets( + service, (query: any) => context.prepareQuery(query), this.query, this.query.facets); + } + + return result; + } + + private async executeForRowsInternal(context: UserContext): Promise>> { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + return new SmartList>( + await service.executeQuery(context.prepareQuery(this.query)) as Record[]); + } + + private async executeCountedListInternal( + context: UserContext, + ): Promise> { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + const preparedQuery = context.prepareQuery(this.query); + const useIdSet = preparedQuery.localIdSetPaginationOptions() !== undefined; + const countOperation = { kind: "named-count" as const, + method: this._pageCountMethod ?? "count", + ...(this._pageCountMethod === "countAs" ? { alias: this._pageCountAlias } : {}) }; + const totalCountBeforeRows = useIdSet ? undefined : await service.executeCount( + preparedQuery, this._pageCountAlias, countOperation); + const pageQuery = preparedQuery.clone(); + // A page count describes the full filtered set, not an aggregate-only page row. + pageQuery.aggregateItems = pageQuery.aggregateItems.filter(item => + !(String(item.function).toLowerCase() === "count" && item.alias === this._pageCountAlias)); + const rows = await service.executeQuery(pageQuery); + const totalCount = useIdSet && context.idSetPaginationCountAccuracy === "EXACT" + ? context.idSetPaginationCount! + : (totalCountBeforeRows ?? await service.executeCount(preparedQuery, this._pageCountAlias, countOperation)); + const data = new SmartList(rows.map((row: unknown) => + row instanceof Shipment + ? row + : Shipment.fromRecord(row as Record, new EntityRoot()))); + data.totalCount = totalCount; + if (this._pageCountAlias !== undefined) data.aggregations[this._pageCountAlias] = totalCount; + if (this.query.facets.length > 0) { + data.facets = await executeRelationFacets( + service, (query: any) => context.prepareQuery(query), preparedQuery, preparedQuery.facets); + } + return data; + } + + private executeForStreamInternal(context: UserContext, chunkSize: number): AsyncIterable { + this.ensureIntent(); + if (this.filters.length > 0) this.query.filter({ "$and": this.filters }); + const service = context.requireResource("dataService"); + const preparedQuery = context.prepareQuery(this.query); + // Prepare Context and capture the runtime request before returning a lazy iterator. + const chunks = service.executeForStream(preparedQuery, chunkSize); + return (async function* () { + for await (const chunk of chunks) { + for (const entity of chunk) { + yield entity instanceof Shipment + ? entity + : Shipment.fromRecord(entity as Record, new EntityRoot()); + } + } + })(); + } + + private ensureIntent(): void { + // Runtime owns validation, Unicode whitespace rules and typed diagnostics. + new QueryIntent(this._comment, this._purpose); + } + +} + +export class ExecutableShipmentRequest { + constructor( + private readonly execute: (context: UserContext) => Promise>, + private readonly executeRows: (context: UserContext) => Promise>>, + private readonly stream: (context: UserContext, chunkSize: number) => AsyncIterable, + private readonly limitOne: () => void, + private readonly addComment: (comment: string) => void, + private readonly ensureIntent: () => void, + ) {} + + comment(c: string): this { + this.addComment(c); + return this; + } + + newEntity(context: UserContext): Shipment { + this.ensureIntent(); + return new Shipment(); + } + + executeForList(context: UserContext): Promise> { + this.ensureIntent(); + return this.execute(context); + } + + executeForRows(context: UserContext): Promise>> { + this.ensureIntent(); + return this.executeRows(context); + } + + executeForStream(context: UserContext, chunkSize = 1000): AsyncIterable { + this.ensureIntent(); + return this.stream(context, chunkSize); + } + + async executeForOne(context: UserContext): Promise { + this.limitOne(); + return (await this.executeForList(context))[0]; + } +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/runtime-module.ts b/source/teaql-ts/examples/trace-chain/lib/src/runtime-module.ts new file mode 100644 index 0000000000000000000000000000000000000000..a8b437b328f051d380d01fb77062da76e4a7c641 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/runtime-module.ts @@ -0,0 +1,144 @@ +import { ObjectLocation, RuntimeModule } from "teaql-ts"; +import { ENTITY_SCHEMAS } from "./teaql-schemas"; +import { ensureGeneratedBootstrap } from "./generated-bootstrap"; + +/** Generated canonical KSML to selected-wire mapping. Never infer this from TS members. */ +export const GENERATED_WIRE_MODEL_METADATA = Object.freeze({ + profile: "camelCase" as const, + entities: Object.freeze({ + "Platform": Object.freeze({ + entityType: "Platform", + profile: "camelCase" as const, + fields: Object.freeze({ + "id": Object.freeze({ canonicalName: "id", wireName: "id", aliases: Object.freeze([] as string[]) }), + "name": Object.freeze({ canonicalName: "name", wireName: "name", aliases: Object.freeze([] as string[]) }), + "version": Object.freeze({ canonicalName: "version", wireName: "version", aliases: Object.freeze([] as string[]) }) + }), + }), + "CustomerOrder": Object.freeze({ + entityType: "CustomerOrder", + profile: "camelCase" as const, + fields: Object.freeze({ + "id": Object.freeze({ canonicalName: "id", wireName: "id", aliases: Object.freeze([] as string[]) }), + "platform": Object.freeze({ canonicalName: "platform", wireName: "platform", aliases: Object.freeze([] as string[]) }), + "order_number": Object.freeze({ canonicalName: "order_number", wireName: "orderNumber", aliases: Object.freeze([] as string[]) }), + "description": Object.freeze({ canonicalName: "description", wireName: "description", aliases: Object.freeze([] as string[]) }), + "version": Object.freeze({ canonicalName: "version", wireName: "version", aliases: Object.freeze([] as string[]) }) + }), + }), + "OrderItem": Object.freeze({ + entityType: "OrderItem", + profile: "camelCase" as const, + fields: Object.freeze({ + "id": Object.freeze({ canonicalName: "id", wireName: "id", aliases: Object.freeze([] as string[]) }), + "customer_order": Object.freeze({ canonicalName: "customer_order", wireName: "customerOrder", aliases: Object.freeze([] as string[]) }), + "name": Object.freeze({ canonicalName: "name", wireName: "name", aliases: Object.freeze([] as string[]) }), + "version": Object.freeze({ canonicalName: "version", wireName: "version", aliases: Object.freeze([] as string[]) }) + }), + }), + "Payment": Object.freeze({ + entityType: "Payment", + profile: "camelCase" as const, + fields: Object.freeze({ + "id": Object.freeze({ canonicalName: "id", wireName: "id", aliases: Object.freeze([] as string[]) }), + "customer_order": Object.freeze({ canonicalName: "customer_order", wireName: "customerOrder", aliases: Object.freeze([] as string[]) }), + "reference_code": Object.freeze({ canonicalName: "reference_code", wireName: "referenceCode", aliases: Object.freeze([] as string[]) }), + "version": Object.freeze({ canonicalName: "version", wireName: "version", aliases: Object.freeze([] as string[]) }) + }), + }), + "PaymentAttempt": Object.freeze({ + entityType: "PaymentAttempt", + profile: "camelCase" as const, + fields: Object.freeze({ + "id": Object.freeze({ canonicalName: "id", wireName: "id", aliases: Object.freeze([] as string[]) }), + "payment": Object.freeze({ canonicalName: "payment", wireName: "payment", aliases: Object.freeze([] as string[]) }), + "reference_code": Object.freeze({ canonicalName: "reference_code", wireName: "referenceCode", aliases: Object.freeze([] as string[]) }), + "version": Object.freeze({ canonicalName: "version", wireName: "version", aliases: Object.freeze([] as string[]) }) + }), + }), + "Shipment": Object.freeze({ + entityType: "Shipment", + profile: "camelCase" as const, + fields: Object.freeze({ + "id": Object.freeze({ canonicalName: "id", wireName: "id", aliases: Object.freeze([] as string[]) }), + "customer_order": Object.freeze({ canonicalName: "customer_order", wireName: "customerOrder", aliases: Object.freeze([] as string[]) }), + "reference_code": Object.freeze({ canonicalName: "reference_code", wireName: "referenceCode", aliases: Object.freeze([] as string[]) }), + "version": Object.freeze({ canonicalName: "version", wireName: "version", aliases: Object.freeze([] as string[]) }) + }), + }) + }), +}); + +/** Passive generated metadata manifest. It never modifies the database schema. */ +export const GENERATED_RUNTIME_MODULE = new RuntimeModule(ENTITY_SCHEMAS, { + "Platform": { + checkAndFix(context, mutation, results) { + const now = context.getResource("fixTime"); + if ((mutation.action === "Create" && mutation.payload["name"] === undefined) || mutation.payload["name"] === null) results.push({ ruleId: "required", location: ObjectLocation.property("name") }); + if (mutation.payload["name"] != null && [...String(mutation.payload["name"])].length > 100) results.push({ ruleId: "max_length", location: ObjectLocation.property("name"), inputValue: mutation.payload["name"], systemValue: 100 }); + + + }, + }, + "CustomerOrder": { + checkAndFix(context, mutation, results) { + const now = context.getResource("fixTime"); + if ((mutation.action === "Create" && mutation.payload["platform"] === undefined) || mutation.payload["platform"] === null) results.push({ ruleId: "required", location: ObjectLocation.property("platform") }); + + if ((mutation.action === "Create" && mutation.payload["order_number"] === undefined) || mutation.payload["order_number"] === null) results.push({ ruleId: "required", location: ObjectLocation.property("order_number") }); + if (mutation.payload["order_number"] != null && [...String(mutation.payload["order_number"])].length > 100) results.push({ ruleId: "max_length", location: ObjectLocation.property("order_number"), inputValue: mutation.payload["order_number"], systemValue: 100 }); + + if ((mutation.action === "Create" && mutation.payload["description"] === undefined) || mutation.payload["description"] === null) results.push({ ruleId: "required", location: ObjectLocation.property("description") }); + if (mutation.payload["description"] != null && [...String(mutation.payload["description"])].length > 100) results.push({ ruleId: "max_length", location: ObjectLocation.property("description"), inputValue: mutation.payload["description"], systemValue: 100 }); + + + }, + }, + "OrderItem": { + checkAndFix(context, mutation, results) { + const now = context.getResource("fixTime"); + if ((mutation.action === "Create" && mutation.payload["customer_order"] === undefined) || mutation.payload["customer_order"] === null) results.push({ ruleId: "required", location: ObjectLocation.property("customer_order") }); + + if ((mutation.action === "Create" && mutation.payload["name"] === undefined) || mutation.payload["name"] === null) results.push({ ruleId: "required", location: ObjectLocation.property("name") }); + if (mutation.payload["name"] != null && [...String(mutation.payload["name"])].length > 100) results.push({ ruleId: "max_length", location: ObjectLocation.property("name"), inputValue: mutation.payload["name"], systemValue: 100 }); + + + }, + }, + "Payment": { + checkAndFix(context, mutation, results) { + const now = context.getResource("fixTime"); + if ((mutation.action === "Create" && mutation.payload["customer_order"] === undefined) || mutation.payload["customer_order"] === null) results.push({ ruleId: "required", location: ObjectLocation.property("customer_order") }); + + if ((mutation.action === "Create" && mutation.payload["reference_code"] === undefined) || mutation.payload["reference_code"] === null) results.push({ ruleId: "required", location: ObjectLocation.property("reference_code") }); + if (mutation.payload["reference_code"] != null && [...String(mutation.payload["reference_code"])].length > 100) results.push({ ruleId: "max_length", location: ObjectLocation.property("reference_code"), inputValue: mutation.payload["reference_code"], systemValue: 100 }); + + + }, + }, + "PaymentAttempt": { + checkAndFix(context, mutation, results) { + const now = context.getResource("fixTime"); + if ((mutation.action === "Create" && mutation.payload["payment"] === undefined) || mutation.payload["payment"] === null) results.push({ ruleId: "required", location: ObjectLocation.property("payment") }); + + if ((mutation.action === "Create" && mutation.payload["reference_code"] === undefined) || mutation.payload["reference_code"] === null) results.push({ ruleId: "required", location: ObjectLocation.property("reference_code") }); + if (mutation.payload["reference_code"] != null && [...String(mutation.payload["reference_code"])].length > 100) results.push({ ruleId: "max_length", location: ObjectLocation.property("reference_code"), inputValue: mutation.payload["reference_code"], systemValue: 100 }); + + + }, + }, + "Shipment": { + checkAndFix(context, mutation, results) { + const now = context.getResource("fixTime"); + if ((mutation.action === "Create" && mutation.payload["customer_order"] === undefined) || mutation.payload["customer_order"] === null) results.push({ ruleId: "required", location: ObjectLocation.property("customer_order") }); + + if ((mutation.action === "Create" && mutation.payload["reference_code"] === undefined) || mutation.payload["reference_code"] === null) results.push({ ruleId: "required", location: ObjectLocation.property("reference_code") }); + if (mutation.payload["reference_code"] != null && [...String(mutation.payload["reference_code"])].length > 100) results.push({ ruleId: "max_length", location: ObjectLocation.property("reference_code"), inputValue: mutation.payload["reference_code"], systemValue: 100 }); + + + }, + } +}, { ...{ + defaultDomainRoot: { entity: "Platform", id: "1", values: { "name": "Trace Chain Verification" } }, + constants: [] +}, ensure: ensureGeneratedBootstrap }); \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/teaql-schemas.ts b/source/teaql-ts/examples/trace-chain/lib/src/teaql-schemas.ts new file mode 100644 index 0000000000000000000000000000000000000000..ea1f661d8b38191404170172b53ad3a3b59f66f2 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/teaql-schemas.ts @@ -0,0 +1,40 @@ +import { EntitySchema } from "teaql-ts/sql/core"; + +export const ENTITY_SCHEMAS: Record = { +"Platform": { + table: "platform_data", + auditMaskFields: [], + columns: {"id": { columnName: "id", modelName: "id", logPolicy: "plain", logicalType: "integer", decode: "string", nullable: false }, "name": { columnName: "name", modelName: "name", logPolicy: "plain", logicalType: "text", decode: "native", nullable: false }, "version": { columnName: "version", modelName: "version", logPolicy: "plain", logicalType: "integer", decode: "number", nullable: false }}, + relations: {"customerOrderList": { targetEntity: "CustomerOrder", localKey: "id", foreignKey: "platform", many: true }} +}, +"CustomerOrder": { + table: "customer_order_data", + auditMaskFields: [], + columns: {"id": { columnName: "id", modelName: "id", logPolicy: "plain", logicalType: "integer", decode: "string", nullable: false }, "platform": { columnName: "platform", modelName: "platform", logPolicy: "plain", logicalType: "integer", decode: "string", nullable: false }, "orderNumber": { columnName: "order_number", modelName: "order_number", logPolicy: "plain", logicalType: "text", decode: "native", nullable: false }, "description": { columnName: "description", modelName: "description", logPolicy: "plain", logicalType: "text", decode: "native", nullable: false }, "version": { columnName: "version", modelName: "version", logPolicy: "plain", logicalType: "integer", decode: "number", nullable: false }}, + relations: {"platform": { targetEntity: "Platform", localKey: "platform", foreignKey: "id", many: false }, "orderItemList": { targetEntity: "OrderItem", localKey: "id", foreignKey: "customerOrder", many: true }, "paymentList": { targetEntity: "Payment", localKey: "id", foreignKey: "customerOrder", many: true }, "shipmentList": { targetEntity: "Shipment", localKey: "id", foreignKey: "customerOrder", many: true }} +}, +"OrderItem": { + table: "order_item_data", + auditMaskFields: ["name"], + columns: {"id": { columnName: "id", modelName: "id", logPolicy: "plain", logicalType: "integer", decode: "string", nullable: false }, "customerOrder": { columnName: "customer_order", modelName: "customer_order", logPolicy: "plain", logicalType: "integer", decode: "string", nullable: false }, "name": { columnName: "name", modelName: "name", logPolicy: "plain", logicalType: "text", decode: "native", nullable: false }, "version": { columnName: "version", modelName: "version", logPolicy: "plain", logicalType: "integer", decode: "number", nullable: false }}, + relations: {"customerOrder": { targetEntity: "CustomerOrder", localKey: "customerOrder", foreignKey: "id", many: false }} +}, +"Payment": { + table: "payment_data", + auditMaskFields: [], + columns: {"id": { columnName: "id", modelName: "id", logPolicy: "plain", logicalType: "integer", decode: "string", nullable: false }, "customerOrder": { columnName: "customer_order", modelName: "customer_order", logPolicy: "plain", logicalType: "integer", decode: "string", nullable: false }, "referenceCode": { columnName: "reference_code", modelName: "reference_code", logPolicy: "plain", logicalType: "text", decode: "native", nullable: false }, "version": { columnName: "version", modelName: "version", logPolicy: "plain", logicalType: "integer", decode: "number", nullable: false }}, + relations: {"customerOrder": { targetEntity: "CustomerOrder", localKey: "customerOrder", foreignKey: "id", many: false }, "paymentAttemptList": { targetEntity: "PaymentAttempt", localKey: "id", foreignKey: "payment", many: true }} +}, +"PaymentAttempt": { + table: "payment_attempt_data", + auditMaskFields: [], + columns: {"id": { columnName: "id", modelName: "id", logPolicy: "plain", logicalType: "integer", decode: "string", nullable: false }, "payment": { columnName: "payment", modelName: "payment", logPolicy: "plain", logicalType: "integer", decode: "string", nullable: false }, "referenceCode": { columnName: "reference_code", modelName: "reference_code", logPolicy: "plain", logicalType: "text", decode: "native", nullable: false }, "version": { columnName: "version", modelName: "version", logPolicy: "plain", logicalType: "integer", decode: "number", nullable: false }}, + relations: {"payment": { targetEntity: "Payment", localKey: "payment", foreignKey: "id", many: false }} +}, +"Shipment": { + table: "shipment_data", + auditMaskFields: [], + columns: {"id": { columnName: "id", modelName: "id", logPolicy: "plain", logicalType: "integer", decode: "string", nullable: false }, "customerOrder": { columnName: "customer_order", modelName: "customer_order", logPolicy: "plain", logicalType: "integer", decode: "string", nullable: false }, "referenceCode": { columnName: "reference_code", modelName: "reference_code", logPolicy: "plain", logicalType: "text", decode: "native", nullable: false }, "version": { columnName: "version", modelName: "version", logPolicy: "plain", logicalType: "integer", decode: "number", nullable: false }}, + relations: {"customerOrder": { targetEntity: "CustomerOrder", localKey: "customerOrder", foreignKey: "id", many: false }} +} +}; \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/src/teaql-ts.ts b/source/teaql-ts/examples/trace-chain/lib/src/teaql-ts.ts new file mode 100644 index 0000000000000000000000000000000000000000..b54a29ac3c12fb072cf4c292e22bb2e644b56319 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/src/teaql-ts.ts @@ -0,0 +1,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(intent: MutationIntent, work: (graph: GraphMutationSession) => Promise): Promise; + preflightMutation(mutation: any): any; + afterGraphCommit(work: () => void): void; + afterGraphRollback(work: () => void): void; + executeMutation(mutation: any): Promise; + executeQuery(query: any): Promise; + executeCount(query: any, alias?: string, operation?: QueryOperation): Promise; + executeForStream(query: any, chunkSize?: number): AsyncIterable; + close?(): Promise; +} + +export class TeaQLClient implements TeaQLDataService { + private data: Record> = {}; + private nextIds: Record = {}; + private readonly checkers: Record = {}; + private userContext = new UserContext(); + private graphSaveActive = false; + private graphCommitActions: Array<() => void> = []; + private graphRollbackActions: Array<() => void> = []; + private graphSaveTail: Promise = 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(intent: MutationIntent, work: (graph: GraphMutationSession) => Promise): Promise { + const graph = new GraphMutationSession(intent); + const predecessor = this.graphSaveTail; + let release!: () => void; + this.graphSaveTail = new Promise(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 { + 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 { + return { rows: [] }; + } + async executeQuery(query: any): Promise { + 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 { + 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 { + 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}; } \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/teaql-i18n.json b/source/teaql-ts/examples/trace-chain/lib/teaql-i18n.json new file mode 100644 index 0000000000000000000000000000000000000000..f63a0008296eb8143507cfdff3ab5c41e5ef2703 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/teaql-i18n.json @@ -0,0 +1,126 @@ +{ + "schema": "teaql.i18n/v1", + "defaultLocale": "en", + "locales": { + "de": { + "vocabulary": { + }, + "messages": { + } + }, + "ko": { + "vocabulary": { + }, + "messages": { + } + }, + "pt": { + "vocabulary": { + }, + "messages": { + } + }, + "zh-TW": { + "vocabulary": { + }, + "messages": { + } + }, + "fil": { + "vocabulary": { + }, + "messages": { + } + }, + "en": { + "vocabulary": { + "property.platform.name": "Name", + "property.shipment.version": "Version", + "property.customerOrder.orderNumber": "Order Number", + "property.orderItem.id": "Id", + "property.shipment.referenceCode": "Reference Code", + "property.customerOrder.description": "Description", + "entity.orderItem": "Order Item", + "property.orderItem.customerOrder": "Customer Order", + "property.orderItem.name": "Name", + "entity.customerOrder": "Customer Order", + "property.payment.customerOrder": "Customer Order", + "property.customerOrder.id": "Id", + "property.paymentAttempt.version": "Version", + "property.payment.version": "Version", + "property.shipment.customerOrder": "Customer Order", + "property.paymentAttempt.id": "Id", + "property.customerOrder.version": "Version", + "property.payment.id": "Id", + "entity.platform": "Platform", + "property.platform.id": "Id", + "property.customerOrder.platform": "Platform", + "entity.payment": "Payment", + "property.shipment.id": "Id", + "property.payment.referenceCode": "Reference Code", + "property.paymentAttempt.referenceCode": "Reference Code", + "entity.shipment": "Shipment", + "property.orderItem.version": "Version", + "property.platform.version": "Version", + "property.paymentAttempt.payment": "Payment", + "entity.paymentAttempt": "Payment Attempt" + }, + "messages": { + } + }, + "fr": { + "vocabulary": { + }, + "messages": { + } + }, + "zh-CN": { + "vocabulary": { + }, + "messages": { + } + }, + "es": { + "vocabulary": { + }, + "messages": { + } + }, + "ar": { + "vocabulary": { + }, + "messages": { + } + }, + "vi": { + "vocabulary": { + }, + "messages": { + } + }, + "th": { + "vocabulary": { + }, + "messages": { + } + }, + "uk": { + "vocabulary": { + }, + "messages": { + } + }, + "ja": { + "vocabulary": { + }, + "messages": { + } + }, + "id": { + "vocabulary": { + }, + "messages": { + } + } + } +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/lib/tsconfig.json b/source/teaql-ts/examples/trace-chain/lib/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..18139f743ca48c921f9a21e51be3b1485651513c --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/lib/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true + }, + "include": ["src/**/*", "*.d.ts"] +} \ No newline at end of file diff --git a/source/teaql-ts/examples/trace-chain/model.xml b/source/teaql-ts/examples/trace-chain/model.xml new file mode 100644 index 0000000000000000000000000000000000000000..686ff58ad4f57990424a5b4ac99bbe80cb6bc2bd --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/model.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/source/teaql-ts/examples/trace-chain/package-lock.json b/source/teaql-ts/examples/trace-chain/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..d95a81eaa421503632d7cc1f87b14bb00d9a8897 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/package-lock.json @@ -0,0 +1,826 @@ +{ + "name": "teaql-typescript-trace-chain-example", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "teaql-typescript-trace-chain-example", + "dependencies": { + "better-sqlite3": "^12.10.0", + "teaql-ts": "file:../.." + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.0.0", + "ts-node": "^10.9.2", + "typescript": "5.4.5" + } + }, + "../..": { + "name": "@teaql/teaql", + "version": "0.3.1", + "license": "Apache-2.0", + "dependencies": { + "node-fetch": "^2.7.0" + }, + "devDependencies": { + "@aws-sdk/client-s3": "3.1151.0", + "@aws-sdk/s3-request-presigner": "3.1151.0", + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/context-async-hooks": "^2.10.0", + "@opentelemetry/core": "^2.10.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.221.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.221.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/resources": "^2.10.0", + "@opentelemetry/sdk-logs": "^0.221.0", + "@opentelemetry/sdk-metrics": "^2.10.0", + "@opentelemetry/sdk-trace-base": "^2.10.0", + "@sqlite.org/sqlite-wasm": "3.53.0-build1", + "@types/better-sqlite3": "^7.6.12", + "@types/jest": "^29.5.0", + "@types/node-fetch": "^2.6.13", + "@types/pg": "^8.11.0", + "better-sqlite3": "^12.10.0", + "esbuild": "^0.28.2", + "jest": "^29.5.0", + "jest-util": "^29.7.0", + "mysql2": "^3.11.0", + "peggy": "^5.1.0", + "pg": "^8.12.0", + "pg-query-stream": "^4.7.0", + "ts-jest": "^29.1.0", + "typescript": "5.4.5" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "3.1151.0", + "@aws-sdk/s3-request-presigner": "3.1151.0", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/context-async-hooks": "^2.10.0", + "@opentelemetry/core": "^2.10.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.221.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.221.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/resources": "^2.10.0", + "@opentelemetry/sdk-logs": "^0.221.0", + "@opentelemetry/sdk-metrics": "^2.10.0", + "@opentelemetry/sdk-trace-base": "^2.10.0", + "@sqlite.org/sqlite-wasm": "3.53.0-build1", + "better-sqlite3": "^12.10.0", + "expo-sqlite": "*", + "mysql2": "^3.11.0", + "pg": "^8.12.0", + "pg-query-stream": "^4.7.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-s3": { + "optional": true + }, + "@aws-sdk/s3-request-presigner": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/api-logs": { + "optional": true + }, + "@opentelemetry/context-async-hooks": { + "optional": true + }, + "@opentelemetry/core": { + "optional": true + }, + "@opentelemetry/exporter-logs-otlp-http": { + "optional": true + }, + "@opentelemetry/exporter-metrics-otlp-http": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-http": { + "optional": true + }, + "@opentelemetry/otlp-transformer": { + "optional": true + }, + "@opentelemetry/resources": { + "optional": true + }, + "@opentelemetry/sdk-logs": { + "optional": true + }, + "@opentelemetry/sdk-metrics": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "@sqlite.org/sqlite-wasm": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-query-stream": { + "optional": true + } + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", + "integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.96.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", + "integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/teaql-ts": { + "resolved": "../..", + "link": true + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/source/teaql-ts/examples/trace-chain/package.json b/source/teaql-ts/examples/trace-chain/package.json new file mode 100644 index 0000000000000000000000000000000000000000..e8ea5d9e4532218e954cee27dd7248f606678931 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/package.json @@ -0,0 +1,7 @@ +{ + "name": "teaql-typescript-trace-chain-example", + "private": true, + "scripts": { "build": "tsc --noEmit", "start": "ts-node app.ts", "test:shared-reference": "ts-node shared-reference.ts", "test:page": "ts-node paging.ts", "test:stream-capture": "ts-node stream-capture.ts", "test:relation-aggregate": "ts-node relation-aggregate.ts", "test:checker-overlap": "ts-node checker-overlap.ts", "test:plan-item-lineage": "ts-node plan-item-lineage.ts", "test:bootstrap-intent": "ts-node bootstrap-intent.ts", "test:standard-telemetry": "ts-node standard-telemetry.ts", "test:standard-telemetry-graph": "ts-node standard-telemetry-graph.ts" }, + "dependencies": { "teaql-ts": "file:../..", "better-sqlite3": "^12.10.0" }, + "devDependencies": { "typescript": "5.4.5", "ts-node": "^10.9.2", "@types/node": "^22.0.0", "@types/better-sqlite3": "^7.6.12" } +} diff --git a/source/teaql-ts/examples/trace-chain/paging.ts b/source/teaql-ts/examples/trace-chain/paging.ts new file mode 100644 index 0000000000000000000000000000000000000000..e0485eefbc3c55b225db7ba9850e989ce7e5f022 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/paging.ts @@ -0,0 +1,124 @@ +import assert from 'node:assert/strict'; +import { randomUUID, createHash } from 'node:crypto'; +import { mkdirSync, readFileSync, readdirSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { EntityRoot, SelectQuery, UserContext } from 'teaql-ts'; +import { AbstractSQLTeaQLClient, SQLExecutionEvidenceStore } from 'teaql-ts/sql/core'; +import { SQLiteDriver } from 'teaql-ts/sql/sqlite'; +import { Q } from './lib/src/generated/Q'; +import { E } from './lib/src/generated/E'; +import { GENERATED_RUNTIME_MODULE } from './lib/src/runtime-module'; + +function manifest(directory: string, prefix = ''): string { + return readdirSync(directory,{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name)) + .map(entry=>entry.isDirectory()?manifest(join(directory,entry.name),`${prefix}${entry.name}/`) + :`${createHash('sha256').update(readFileSync(join(directory,entry.name))).digest('hex')} ${prefix}${entry.name}\n`).join(''); +} +class Client extends AbstractSQLTeaQLClient { + constructor(driver:SQLiteDriver){super(driver,GENERATED_RUNTIME_MODULE.schemas);} +} +class Context extends UserContext { + allowed?:string[]; + calls=0; + override prepareQuery(query:SelectQuery):SelectQuery { + this.calls++; + const prepared=super.prepareQuery(query); + if(this.allowed && prepared.entity==='CustomerOrder') prepared.filter({$and:[prepared.filterCondition,{id:{$in:this.allowed}}]}); + return prepared; + } +} +const ledger=(entity:unknown)=>(entity as {_root:EntityRoot})._root; +async function main() { + const before=manifest('lib'); + const database=resolve(process.env.TEAQL_TRACE_CHAIN_PAGE_DB??'.local/paging.sqlite'); + mkdirSync(dirname(database),{recursive:true}); + const client=new Client(new SQLiteDriver(database)).install(GENERATED_RUNTIME_MODULE).setDiagnosticSQLLogSink(undefined); + const context=new Context().insertResource('dataService',client); + const sql=new SQLExecutionEvidenceStore(); + const audits:Readonly>[]=[]; + client.setRuntimeTelemetrySink(sql).setAuditSink(event=>{audits.push(event);}); + const failures:unknown[]=[]; + try { + await context.ensureSchema(); await context.ensureSchema(); + for(const mode of ['page','scope','list','stream']) { + try { + context.allowed=undefined; + const group=`VISIBLE-PAGE-${randomUUID()}`; + const privateName='PRIVATE-TS-PAGE-CHILD-CANARY'; + const ids:string[]=[]; + for(let i=0;i<(mode==='scope'?4:3);i++) { + const root=Q.customerOrders().comment('initialize page root').purpose('verify page isolation').newEntity(context) + .updatePlatform('1').updateOrderNumber(`${group}-${i}`).updateDescription(group); + root.orderItemList().push(Q.orderItems().comment('initialize page child').purpose('verify page isolation').newEntity(context).updateName(privateName)); + await root.auditAs('seed page graph').save(context); ids.push(root.id!); + } + const paged=mode==='page'||mode==='scope'; + if(mode==='scope')context.allowed=ids.slice(0,3); + if(!paged)context.allowed=ids.slice(1,3); + sql.enableAll();audits.length=0;context.calls=0; + const request=Q.customerOrders().withDescriptionIs(group) + .selectOrderItemListWith(Q.orderItems().withNameIs(privateName).limit(2)) + .orderByIdAscending().limit(3).comment(`inspect ${privateName} ${group}`); + let page; + if(paged)page=await request.offset(1).limit(2).count() + .purpose(`verify ${privateName} ${group}`).executeForList(context); + else { + const executable=request.purpose(`verify ${privateName} ${group}`); + const data=mode==='list'?Array.from(await executable.executeForList(context)):[]; + if(mode==='stream')for await(const entity of executable.executeForStream(context,1))data.push(entity); + page={data,totalCount:undefined}; + } + assert.equal(page.totalCount,paged?3:undefined,'count and data must use the same Context-scoped request'); + assert.equal(context.calls,1,'prepare Context once for count and rows'); + const rows=Array.from(page.data); + assert.deepEqual(rows.map(row=>E.customerOrder(row).id().eval()),ids.slice(1,3)); + assert.notEqual(ledger(rows[0]),ledger(rows[1]),'page roots need independent mutation ownership'); + const childIDs=rows.map((row,i)=>{ + assert.equal(row.orderItemList().length,1); + const child=row.orderItemList()[0]; + assert.equal(ledger(row),ledger(child),'owned child must join only its parent graph'); + assert.equal(E.orderItem(child).name().eval(),privateName); + assert.equal(E.orderItem(child).customerOrderId().eval(),ids[i+1]); + return child.id!; + }); + const statements=sql.snapshot(); + assert.equal(statements.filter(entry=>entry.traceChain?.some(node=> + node.kind==='comment' && node.detail==='count()')).length,paged?1:0, + 'only explicit count() creates the total query; no retired page-route alias'); + assert.equal(statements.length,paged?4:3,'only count (if requested), root and two SQLite child probes execute'); + assert(!JSON.stringify(statements).includes(privateName),'page/count/relation logs must mask child source'); + for(const entry of statements){ + assert(entry.comment?.includes(group));assert(entry.purpose?.includes(group)); + assert.equal(entry.tracePath[0].name,'CustomerOrder'); + assert.equal(entry.tracePath[entry.tracePath.length-1].kind,'sql'); + } + context.allowed=undefined; + rows.forEach((row,i)=>{ + row.updateDescription(`${group}-saved-${i}`); + row.orderItemList()[0].updateName(`saved child ${i}`).auditAs(`edit page child ${i}`); + }); + for(let i=0;i<2;i++) { + audits.length=0;sql.enableAll(); + const reason=`save independent page ${i}`; + await rows[i].auditAs(reason).save(context); + assert.equal(audits.length,2,'only owning root and child commit'); + for(const event of audits)assert.equal((event.mutationLineage as any[])[0].detail,reason); + for(let j=0;j<2;j++) { + const reloaded=await Q.customerOrders().withIdIs(ids[j+1]).selectOrderItemListWith(Q.orderItems().limit(2)).limit(1) + .comment('reload page graph').purpose('verify independent persistence').executeForList(context); + const root=reloaded[0],child=root.orderItemList()[0]; + assert.equal(E.customerOrder(root).description().eval(),j<=i?`${group}-saved-${j}`:group); + assert.equal(E.customerOrder(root).version().eval(),j<=i?2:1); + assert.equal(E.orderItem(child).name().eval(),j<=i?`saved child ${j}`:privateName); + assert.equal(E.orderItem(child).version().eval(),j<=i?2:1); + } + } + console.log(`PAGE_OBSERVED ${JSON.stringify({mode,group,ids,childIDs,total:page.totalCount,offset:paged?1:0,size:2,physicalQueries:statements.length})}`); + console.log(`PAGE OWNERSHIP PASSED: ${mode}`); + } catch(error) {failures.push(error);console.error(`PAGE FAILED: ${mode}`,error);} + } + assert.equal(manifest('lib'),before,'generated library unchanged'); + assert.equal(failures.length,0,'all page scenarios must pass'); + } finally {await client.close();} +} +main().catch(error=>{console.error(error);process.exitCode=1;}); diff --git a/source/teaql-ts/examples/trace-chain/plan-item-lineage.ts b/source/teaql-ts/examples/trace-chain/plan-item-lineage.ts new file mode 100644 index 0000000000000000000000000000000000000000..17e553acf5b6b28b82b7b1ae1d876610f4f66fcc --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/plan-item-lineage.ts @@ -0,0 +1,160 @@ +import assert from 'node:assert/strict'; +import { createHash, randomUUID } from 'node:crypto'; +import { mkdirSync, readFileSync, readdirSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { DelegatingMutationPolicyApprovalProvider, DelegatingMutationPolicyRegistry, + MutationPlan, MutationRequest, TraceNode, UserContext } from 'teaql-ts'; +import { AbstractSQLTeaQLClient, MutationResult, SQLExecutionEvidenceStore, + SQLExecutionMetadata, SqlQueryResult, SqlSession } from 'teaql-ts/sql/core'; +import { SQLiteDriver } from 'teaql-ts/sql/sqlite'; +import { Q } from './lib/src/generated/Q'; +import { E } from './lib/src/generated/E'; +import { GENERATED_RUNTIME_MODULE } from './lib/src/runtime-module'; + +// Byte fingerprints only; generated source is not used for API discovery. +function fingerprint(directory: string, prefix = ''): string { + return readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)) + .map(entry => entry.isDirectory() ? fingerprint(join(directory, entry.name), `${prefix}${entry.name}/`) + : `${createHash('sha256').update(readFileSync(join(directory, entry.name))).digest('hex')} ${prefix}${entry.name}\n`).join(''); +} +const originalLibrary = fingerprint('lib'); +const database = resolve(process.env.TEAQL_TRACE_CHAIN_PLAN_DB ?? '.local/plan-item-lineage.sqlite'); +mkdirSync(join(database, '..'), { recursive: true }); +class Driver extends SQLiteDriver { + readonly writes: Array<{ sql: string; values: readonly unknown[]; transaction?: number }> = []; + readonly transactions: Array<{ id: number; committed: boolean }> = []; + activeTransaction?: number; + async transaction(work: (session: SqlSession) => Promise): Promise { + const transaction = { id: this.transactions.length + 1, committed: false }; + this.transactions.push(transaction); + const result = await super.transaction(async session => { + this.activeTransaction = transaction.id; + try { return await work(session); } + finally { this.activeTransaction = undefined; } + }); + transaction.committed = true; + return result; + } + async query(sql: string, values: any[] = []): Promise { + if (/^(INSERT|UPDATE)/.test(sql) && sql.includes('order_item_data')) { + this.writes.push({ sql, values: [...values], transaction: this.activeTransaction }); + } + return super.query(sql, values); + } +} +class Client extends AbstractSQLTeaQLClient { + readonly commands: Array<{ entity: string; id: string; reason: string; lineage: readonly TraceNode[] }> = []; + constructor(driver: Driver) { super(driver, GENERATED_RUNTIME_MODULE.schemas); } + async executeMutation(request: any): Promise { + assert(request instanceof MutationRequest, 'ordinary generated save owns every request'); + const result = await super.executeMutation(request); + this.commands.push({ entity: request.mutation.entity, id: result.id, reason: request.comment, + lineage: request.traceFor({ entity: request.mutation.entity, id: result.id }) }); + return result; + } +} +const plain = (nodes: unknown) => (nodes as readonly TraceNode[]) + .map(node => [node.kind, node.name, node.entityId, node.detail]); + +async function main() { + const driver = new Driver(database); + const client = new Client(driver).install(GENERATED_RUNTIME_MODULE); + const context = new UserContext().insertResource('dataService', client); + client.setUserContext(context); + const evidence = new SQLExecutionEvidenceStore(); + const diagnostics: SQLExecutionMetadata[] = []; + const audits: Readonly>[] = []; + const reviewed: MutationPlan[] = []; + client.setRuntimeTelemetrySink(evidence).setDiagnosticSQLLogSink({ write: entry => diagnostics.push(entry) }) + .setAuditSink(event => { audits.push(event); }); + try { + await context.ensureSchema(); + const identity = { policyId: 'trace-plan-items', version: '1', fingerprint: 'sha256:trace-plan-items-v1' }; + context.withMutationPolicyRegistry(new DelegatingMutationPolicyRegistry(key => + key === 'CustomerOrder.saveGraph' ? { identity, review: (_context, plan) => { + reviewed.push(plan); return { verdict: 'allow' }; + } } : undefined)); + context.withMutationPolicyApprovalProvider(new DelegatingMutationPolicyApprovalProvider(policy => + ({ policy, approvedBy: 'trace-example-owner', approvedAt: new Date('2026-10-04T00:00:00Z') }))); + + for (const logging of [true, false]) { + client.setQueryLoggingEnabled(logging).setMutationLoggingEnabled(logging); + const nonce = randomUUID(); + const order = Q.customerOrders().comment('initialize plan root').purpose('verify ordered graph plan').newEntity(context) + .updatePlatform('1').updateOrderNumber(`PLAN-${nonce}`).updateDescription('before'); + const first = Q.orderItems().comment('initialize first plan item').purpose('verify ordered graph plan').newEntity(context); + const second = Q.orderItems().comment('initialize second plan item').purpose('verify ordered graph plan').newEntity(context); + order.orderItemList().push(first, second); + first.auditAs('prepare first item'); second.auditAs('prepare second item'); + for (const phase of ['create', 'update'] as const) { + const secrets = [`LEFTPRIVATE${phase.toUpperCase()}${nonce}`, `RIGHTPRIVATE${phase.toUpperCase()}${nonce}`]; + first.updateName(secrets[0]); second.updateName(secrets[1]); + order.updateDescription(phase); + const reason = `align ${secrets[0]} with ${secrets[1]}`; + reviewed.length = 0; client.commands.length = 0; driver.writes.length = 0; driver.transactions.length = 0; + diagnostics.length = 0; audits.length = 0; evidence.enableAll(); + await order.auditAs(reason).save(context); + assert.equal(reviewed.length, 1, 'one governed plan, not two independently approved saves'); + const plan = reviewed[0]; + assert.equal(plan.auditReason, reason, 'raw caller intent stays unchanged'); + assert.equal(plan.operations.length, 3); + const items = plan.operations.filter(operation => operation.entity === 'OrderItem'); + assert.equal(items.length, 2); + assert.deepEqual(items.map(operation => operation.changedValues.name), secrets); + assert(items.every(operation => operation.kind === phase)); + assert(Object.isFrozen(plan) && Object.isFrozen(plan.operations)); + const expected = [ + [['auditReason', 'CustomerOrder', order.id, reason]], + [['auditReason', 'CustomerOrder', order.id, reason], ['auditReason', 'OrderItem', first.id, 'prepare first item']], + [['auditReason', 'CustomerOrder', order.id, reason], ['auditReason', 'OrderItem', second.id, 'prepare second item']], + ]; + assert.deepEqual(client.commands.map(command => [command.entity, command.id]), + [['CustomerOrder', order.id], ['OrderItem', first.id], ['OrderItem', second.id]]); + assert.deepEqual(client.commands.map(command => plain(command.lineage)), expected); + assert(client.commands.every(command => command.reason === reason)); + assert.equal(driver.writes.length, 2); + assert.deepEqual(driver.transactions, [{ id: 1, committed: true }], 'one real atomic graph transaction'); + assert(driver.writes.every(write => write.transaction === 1)); + assert(driver.writes[0].values.includes(secrets[0]) && !driver.writes[0].values.includes(secrets[1])); + assert(driver.writes[1].values.includes(secrets[1]) && !driver.writes[1].values.includes(secrets[0])); + const physical = evidence.snapshot(); + assert.equal(physical.length, 6, 'root plus two item writes and three real readbacks'); + assert.equal(audits.length, 3); + assert.equal(diagnostics.length, logging ? 6 : 0); + const safe = JSON.stringify({ physical, diagnostics, audits }); + for (const secret of secrets) assert(!safe.includes(secret), 'sibling secret leaked through inherited prose'); + const safeReason = 'align [REDACTED] with [REDACTED]'; + const safeExpected = expected.map(nodes => nodes.map((node, index) => + index === 0 ? [node[0], node[1], node[2], safeReason] : node)); + for (let index = 0; index < 3; index++) { + const write = physical[index * 2], read = physical[index * 2 + 1]; + assert.equal(write.affectedRows, 1); assert.equal(read.operation, 'select'); assert.equal(read.resultCount, 1); + assert.deepEqual(write.mutationLineage, read.mutationLineage); + assert.deepEqual(audits[index].mutationLineage, write.mutationLineage); + assert.deepEqual(plain(write.mutationLineage), safeExpected[index], 'complete SQL item lineage'); + assert.deepEqual(plain(audits[index].mutationLineage), safeExpected[index], 'complete committed item lineage'); + assert.equal(write.auditReason, safeReason, 'safe write retains the masked root intent'); + assert.equal(read.auditReason, safeReason, 'safe readback retains the masked root intent'); + const leaf = write.mutationLineage![write.mutationLineage!.length - 1]; + assert.equal(leaf.entityId, index === 0 ? order.id : index === 1 ? first.id : second.id); + if (index > 0) assert.equal(leaf.detail, index === 1 ? 'prepare first item' : 'prepare second item'); + } + // The exact same words are not private in an unrelated request that + // neither binds nor loads these items; stale graph masking must not linger. + evidence.enableAll(); + const independent = `independent ${secrets.join(' ')}`; + await Q.platforms().limit(1).comment(independent).purpose('verify completed plan privacy isolation').executeForList(context); + assert.equal(evidence.snapshot()[0].comment, independent); + const loaded = await Q.orderItems().withIdIs(first.id).limit(1) + .comment('read first committed item').purpose('verify item plan persistence').executeForList(context); + assert.equal(E.orderItem(loaded[0]).name().eval(), secrets[0]); + console.log(JSON.stringify({ case: 'TC-MUT-09/TC-REQ-16', path: 'existing MutationPlan and root save', + logging, phase, reviewedPlans: 1, operations: 3, sameTypeItems: 2, + transactions: 1, commands: 3, physicalStatements: 6, committedAudits: 3, siblingPrivacy: true, nextRequestIndependent: true })); + } + } + } finally { await client.close(); } + assert.equal(fingerprint('lib'), originalLibrary); + console.log('PASS TypeScript generated plan item lineage: 4 scenarios; one plan, ordered same-type items, sibling privacy, independent next request'); +} +main().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/source/teaql-ts/examples/trace-chain/relation-aggregate.ts b/source/teaql-ts/examples/trace-chain/relation-aggregate.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca9596a7f52cb0c4fcdae742aee62091b7ebc370 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/relation-aggregate.ts @@ -0,0 +1,168 @@ +import assert from 'node:assert/strict'; +import {createHash, randomUUID} from 'node:crypto'; +import {mkdirSync, readFileSync, readdirSync} from 'node:fs'; +import {dirname, join, resolve} from 'node:path'; +import {UserContext} from 'teaql-ts'; +import {AbstractSQLTeaQLClient, SQLExecutionEvidenceStore} from 'teaql-ts/sql/core'; +import {SQLiteDriver} from 'teaql-ts/sql/sqlite'; +import {Q} from './lib/src/generated/Q'; +import {E} from './lib/src/generated/E'; +import {GENERATED_RUNTIME_MODULE} from './lib/src/runtime-module'; + +type Order = Parameters[0]; +type Payment = Parameters[0]; + +const manifest = (path: string): string => readdirSync(path, {withFileTypes: true}) + .sort((a, b) => a.name.localeCompare(b.name)).map(entry => entry.isDirectory() + ? manifest(join(path, entry.name)) + : createHash('sha256').update(readFileSync(join(path, entry.name))).digest('hex')).join(''); + +class Driver extends SQLiteDriver { + calls = 0; + aggregateFailure?: Error; + override async query(sql: string, values: any[] = []) { + this.calls++; + if (this.aggregateFailure && /COUNT\(/i.test(sql)) throw this.aggregateFailure; + return super.query(sql, values); + } + override async *stream(sql: string, values: any[] = []) { + this.calls++; + yield* super.stream(sql, values); + } +} +class Client extends AbstractSQLTeaQLClient { + constructor(driver: Driver) { super(driver, GENERATED_RUNTIME_MODULE.schemas); } +} +async function collect(stream: AsyncIterable): Promise { + const result: T[] = []; + for await (const row of stream) result.push(row); + return result; +} +function relatedCount(entity: object, alias: string): number { + const value = (entity as unknown as Record)[alias]; + assert.equal(typeof value, 'number', `related count ${alias} was not loaded`); + assert(Number.isSafeInteger(value)); + return value as number; +} + +async function main() { + const before = manifest('lib'); + const path = resolve(process.env.TEAQL_TRACE_CHAIN_AGGREGATE_DB ?? '.local/relation-aggregate.sqlite'); + mkdirSync(dirname(path), {recursive: true}); + const driver = new Driver(path); + const client = new Client(driver).install(GENERATED_RUNTIME_MODULE).setDiagnosticSQLLogSink(undefined); + const context = new UserContext().insertResource('dataService', client); + const evidence = new SQLExecutionEvidenceStore(); + client.setRuntimeTelemetrySink(evidence); + const group = randomUUID(), secret = 'PRIVATE-GENERATED-AGGREGATE', alias = 'matchingItems'; + const ids: string[] = []; + try { + await context.ensureSchema(); + await context.ensureSchema(); + for (let i = 0; i < 2; i++) { + const order = Q.customerOrders().comment('initialize aggregate fixture').purpose('verify generated relation aggregate') + .newEntity(context).updatePlatform('1').updateOrderNumber(`${group}-${i}`).updateDescription(group); + for (const name of [secret, 'ordinary item']) { + order.orderItemList().push(Q.orderItems().comment('initialize item').purpose('verify count predicate') + .newEntity(context).updateName(name)); + } + order.paymentList().push(Q.payments().comment('initialize payment').purpose('verify nested count') + .newEntity(context).updateReferenceCode(`${group}-${i}`)); + await order.auditAs('seed generated aggregate graph').save(context); + ids.push(order.id!); + } + for (const logging of [true, false]) { + client.setQueryLoggingEnabled(logging); + const filtered = await Q.payments().filterByCustomerOrderIn(ids[0]).limit(1) + .selectCustomerOrderWith(Q.customerOrders().withIdIs('0').limit(1)) + .comment('load a filtered forward reference').purpose('distinguish NotLoaded details from null') + .executeForList(context); + assert.equal(filtered.length, 1); + const identity = E.payment(filtered[0]).customerOrder().eval(); + assert(identity, 'a filtered target must retain its actual FK identity'); + assert.equal(E.customerOrder(identity).id().eval(), ids[0]); + assert.throws(() => E.customerOrder(identity).description().eval(), + {name: 'TeaQLNotLoadedError'}, + 'unfetched description must not become a loaded-null value'); + const visible = await Q.payments().filterByCustomerOrderIn(ids[0]).limit(1) + .selectCustomerOrderWith(Q.customerOrders().withIdIs(ids[0]).limit(1)) + .comment('load the full forward reference').purpose('verify independent detail loading') + .executeForList(context); + const full = E.payment(visible[0]).customerOrder().eval(); + assert(full); + assert.equal(E.customerOrder(full).description().eval(), group); + assert.throws(() => E.customerOrder(identity).description().eval(), + {name: 'TeaQLNotLoadedError'}, + 'a later loaded view must not leak into the filtered edge'); + console.log(`FORWARD_NOTLOADED_OBSERVED ${JSON.stringify({logging, id: ids[0]})}`); + } + for (const nested of [false, true]) for (const mode of ['list', 'stream-full', 'stream-tail']) { + for (const logging of [true, false]) { + evidence.enableAll(); + client.setQueryLoggingEnabled(logging); + const child = Q.orderItems().withNameIs(secret).limit(2); + const orders = Q.customerOrders().withIdIn(...ids).limit(2) + .countOrderItemsWith(alias, child) + .selectOrderItemListWith(Q.orderItems().limit(2)); + const payments = logging + ? Q.payments().withCustomerOrderMatching(Q.customerOrders().withIdIn(...ids).limit(2)) + : Q.payments().filterByCustomerOrderIn(...ids); + const query = nested ? payments.limit(2).selectCustomerOrderWith(orders) : orders; + const request = query.comment(`inspect ${secret}`).purpose('verify generated aggregate ancestry'); + const start = driver.calls; + const rows = mode === 'list' ? await request.executeForList(context) + : await collect(request.executeForStream(context, mode === 'stream-full' ? 1 : 3)); + assert.equal(rows.length, 2); + const chunkCount = mode === 'stream-full' ? 2 : 1; + const calls = 1 + 2 + chunkCount * (nested ? 2 : 1); + assert.equal(driver.calls - start, calls, 'actual aggregate and relation SQL must execute even with logs off'); + for (const row of rows) { + // Narrow the public union at its known query boundary, not generated implementation state. + const order = nested ? E.payment(row as Payment).customerOrder().eval() : row as Order; + assert(order); + assert(ids.includes(E.customerOrder(order).id().eval()!)); + assert.equal(relatedCount(order, alias), 1); + assert.equal(E.customerOrder(order).orderItemList().size().eval(), 2); + } + if (logging) { + const entries = evidence.snapshot(); + assert.equal(entries.length, calls); + assert(!JSON.stringify(entries).includes(secret)); + assert(entries.every(entry => entry.tracePath[0].name === (nested ? 'Payment' : 'CustomerOrder'))); + const aggregate = entries.filter(entry => /COUNT\(/i.test(entry.parameterizedSQL)); + assert.equal(aggregate.length, chunkCount); + for (const entry of aggregate) { + assert.deepEqual(entry.tracePath.filter(frame => frame.kind === 'relation').map(frame => frame.name), + nested ? ['customerOrder', 'orderItemList'] : ['orderItemList']); + } + } + console.log(`AGGREGATE_OBSERVED ${JSON.stringify({nested, mode, logging, rows: rows.length, calls})}`); + } + } + client.setQueryLoggingEnabled(true); + const loaded = await Q.customerOrders().withIdIs(ids[0]).countOrderItemsAs(alias) + .selectOrderItemListWith(Q.orderItems().limit(2)).limit(1) + .comment('load graph with a computed projection').purpose('verify aggregate is not mutation data').executeForList(context); + assert.equal(relatedCount(loaded[0], alias), 2); + evidence.enableAll(); + await loaded[0].updateDescription(`saved-${group}`).auditAs('update aggregate-loaded graph').save(context); + const writes = evidence.snapshot().filter(entry => entry.operation === 'update'); + assert.equal(writes.length, 1, 'unchanged children and count projection are not mutations'); + assert(!writes[0].parameterizedSQL.includes(alias)); + const reloaded = await Q.customerOrders().withIdIs(ids[0]).countOrderItemsAs(alias).limit(1) + .comment('recheck saved graph').purpose('verify computed count survives ordinary save').executeForList(context); + assert.equal(E.customerOrder(reloaded[0]).description().eval(), `saved-${group}`); + assert.equal(relatedCount(reloaded[0], alias), 2); + evidence.enableAll(); + const failure = new Error('generated aggregate failure'); + driver.aggregateFailure = failure; + await assert.rejects(collect(Q.customerOrders().withIdIn(...ids).countOrderItemsAs(alias).limit(2) + .comment('reject incomplete aggregate rows').purpose('verify failed stream provenance').executeForStream(context, 1)), + error => error === failure); + assert.deepEqual(evidence.snapshot().map(entry => entry.executionOutcome), ['failure', 'failure']); + driver.aggregateFailure = undefined; + assert.equal(manifest('lib'), before, 'generated library must remain unchanged'); + console.log('PASS TypeScript generated relation aggregates: 12 combinations, audited save and stream failure'); + } finally { await client.close(); } +} +main().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/source/teaql-ts/examples/trace-chain/shared-reference.ts b/source/teaql-ts/examples/trace-chain/shared-reference.ts new file mode 100644 index 0000000000000000000000000000000000000000..985eed9f84519679e0cb8b4c3243e3a092373970 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/shared-reference.ts @@ -0,0 +1,215 @@ +import assert from 'node:assert/strict'; +import { randomUUID, createHash } from 'node:crypto'; +import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { EntityRoot, GraphMutationSession, MutationIntent, MutationRequest, TraceNode, UserContext } from 'teaql-ts'; +import { AbstractSQLTeaQLClient, MutationResult, SQLExecutionEvidenceStore } from 'teaql-ts/sql/core'; +import { SQLiteDriver } from 'teaql-ts/sql/sqlite'; +import { Q } from './lib/src/generated/Q'; +import { E } from './lib/src/generated/E'; +import { GENERATED_RUNTIME_MODULE } from './lib/src/runtime-module'; + +function manifest(directory: string, prefix = ''): string { + return readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)) + .map(entry => entry.isDirectory() ? manifest(join(directory, entry.name), `${prefix}${entry.name}/`) + : `${createHash('sha256').update(readFileSync(join(directory, entry.name))).digest('hex')} ${prefix}${entry.name}\n`).join(''); +} +const before = manifest('lib'); +const nonce = randomUUID(); +const database = resolve(process.env.TEAQL_TRACE_CHAIN_SHARED_DB ?? '.local/shared-reference.sqlite'); +mkdirSync(join(database, '..'), { recursive: true }); +const ledger = (entity: unknown): EntityRoot => (entity as { _root: EntityRoot })._root; +const key = (entity: string, id: string | undefined) => ({ entity, id: id! }); +const plain = (nodes: unknown) => (nodes as readonly TraceNode[]).map(node => [node.kind, node.name, node.entityId, node.detail]); + +class Client extends AbstractSQLTeaQLClient { + readonly commands: Array<{ entity: string; id: string; version?: number; lineage: readonly TraceNode[] }> = []; + readonly snapshots = new Map>>(); + readonly sharedSnapshots: unknown[] = []; + activeSaves = 0; + maxOverlappingSaves = 0; + constructor(driver: SQLiteDriver) { super(driver, GENERATED_RUNTIME_MODULE.schemas); } + async executeQuery(query: any): Promise { + const rows = await super.executeQuery(query); + // Faithfully reuse an immutable provider-loaded reference, never fabricated + // values, expected versions, SQL, trace frames or generated entity wrappers. + for (const row of rows) if (row.platform && typeof row.platform === 'object') { + const cacheKey = `${row.platform.id}:${row.platform.version}`; + let snapshot = this.snapshots.get(cacheKey); + if (!snapshot) { + const loadedSnapshot: Readonly> = Object.freeze({ ...row.platform }); + this.snapshots.set(cacheKey, loadedSnapshot); snapshot = loadedSnapshot; + } + row.platform = snapshot; + this.sharedSnapshots.push(snapshot); + } + return rows; + } + async executeGraphSave(intent: MutationIntent, work: (graph: GraphMutationSession) => Promise): Promise { + this.activeSaves++; + this.maxOverlappingSaves = Math.max(this.maxOverlappingSaves, this.activeSaves); + try { return await super.executeGraphSave(intent, work); } + finally { this.activeSaves--; } + } + async executeMutation(input: any): Promise { + assert(input instanceof MutationRequest); + const result = await super.executeMutation(input); + this.commands.push({ entity: input.mutation.entity, id: result.id, version: input.mutation.version, + lineage: input.traceFor({ entity: input.mutation.entity, id: result.id }) }); + return result; + } +} + +async function main() { + const driver = new SQLiteDriver(database); + const client = new Client(driver).install(GENERATED_RUNTIME_MODULE).setDiagnosticSQLLogSink(undefined); + const context = new UserContext().insertResource('dataService', client); + const sql = new SQLExecutionEvidenceStore(); + const audit: Readonly>[] = []; + client.setRuntimeTelemetrySink(sql).setAuditSink(event => { audit.push(event); }); + const reset = () => { client.commands.length = 0; audit.length = 0; sql.enableAll(); }; + const observe = (scenario: string) => console.log(JSON.stringify({ scenario, + commands: client.commands, sql: sql.snapshot(), committedAudit: audit })); + const create = async (suffix: string, twoItems = false) => { + const order = Q.customerOrders().comment('initialize ownership fixture').purpose('test graph ownership').newEntity(context) + .updatePlatform('1').updateOrderNumber(`${nonce}-${suffix}`).updateDescription(`before-${suffix}`); + for (let index = 0; index < (twoItems ? 2 : 1); index++) { + order.orderItemList().push(Q.orderItems().comment('initialize ownership item').purpose('test graph ownership').newEntity(context) + .updateName(`${nonce}-${suffix}-item-${index}`)); + } + await order.auditAs('create ownership fixture').save(context); + return order; + }; + const load = async (id: string) => { + const rows = await Q.customerOrders().withIdIs(id) + .selectPlatformWith(Q.platforms().limit(1)) + .selectOrderItemListWith(Q.orderItems().limit(10)) + .limit(1).comment('load ownership graph').purpose('test generated graph isolation').executeForList(context); + assert.equal(rows.length, 1); + return rows[0]; + }; + const scenario = process.env.TEAQL_TRACE_CHAIN_SCENARIO; + const checks: string[] = []; + try { + await context.ensureSchema(); await context.ensureSchema(); + if (!scenario || scenario === 'shared') { + const a = await create('shared-a'), b = await create('shared-b'); + await b.updateDescription('give b a distinct optimistic version').auditAs('prepare distinct version').save(context); + const first = await load(a.id!), second = await load(b.id!); + const platformA = E.customerOrder(first).platform().eval()!, platformB = E.customerOrder(second).platform().eval()!; + assert.equal(client.sharedSnapshots[0], client.sharedSnapshots[1], 'provider reference is genuinely shared'); + assert(Object.isFrozen(client.sharedSnapshots[0])); + assert.notEqual(platformA, platformB, 'typed wrappers do not share mutation ownership'); + assert.notEqual(ledger(first), ledger(second)); + assert.notEqual(ledger(platformA), ledger(first)); + assert.notEqual(ledger(platformB), ledger(second)); + const platformRootA = ledger(platformA), platformRootB = ledger(platformB); + const itemA = first.orderItemList()[0], itemB = second.orderItemList()[0]; + const itemVersions = [E.orderItem(itemA).version().eval(), E.orderItem(itemB).version().eval()]; + const rootVersions = [E.customerOrder(first).version().eval()!, E.customerOrder(second).version().eval()!]; + assert.notEqual(rootVersions[0], rootVersions[1]); + first.updateDescription('own first change'); second.updateDescription('own second change'); + reset(); client.maxOverlappingSaves = 0; + await Promise.all([first.auditAs('first independent ownership request').save(context), + second.auditAs('second independent ownership request').save(context)]); + assert.equal(client.maxOverlappingSaves, 2, 'actual public saves overlap; transactions remain queued'); + assert.deepEqual(client.commands.map(command => [command.entity, command.id, command.version]), + [['CustomerOrder', first.id, rootVersions[0]], ['CustomerOrder', second.id, rootVersions[1]]]); + assert.deepEqual([E.orderItem(itemA).version().eval(), E.orderItem(itemB).version().eval()], itemVersions); + assert.equal(ledger(platformA), platformRootA); assert.equal(ledger(platformB), platformRootB); + assert.equal(platformRootA.snapshot().length, 0); assert.equal(platformRootB.snapshot().length, 0); + const expected = [ + [['auditReason', 'CustomerOrder', first.id, 'first independent ownership request']], + [['auditReason', 'CustomerOrder', second.id, 'second independent ownership request']], + ]; + assert.deepEqual(client.commands.map(command => plain(command.lineage)), expected); + assert.deepEqual(audit.map(event => plain(event.mutationLineage)), expected); + assert.deepEqual(sql.snapshot().filter(entry => entry.operation === 'update').map(entry => plain(entry.mutationLineage)), expected); + assert(sql.snapshot().filter(entry => entry.operation === 'update').every(entry => entry.affectedRows === 1)); + observe('shared'); + assert(!sql.snapshot().some(entry => entry.operation === 'update' + && entry.tracePath.some(node => node.kind === 'entity' && node.name === 'Platform'))); + const reread = await load(first.id!); + assert.equal(E.customerOrder(reread).description().eval(), 'own first change'); + assert.equal(E.orderItem(reread.orderItemList()[0]).version().eval(), itemVersions[0]); + checks.push('shared'); + console.log(`SHARED READONLY PASSED: overlapping_saves=2 root_versions=${rootVersions.join(',')} mutations=2 audits=2 unchanged_children=true`); + } + if (!scenario || scenario === 'scoped') { + const original = await create('scoped', true); + const receiver = await load(original.id!), source = await load(original.id!); + const sourceRoot = ledger(source); + source.updateDescription('FOREIGN ROOT MUST REMAIN PENDING'); + const reached = source.orderItemList()[0], unrelated = source.orderItemList()[1]; + reached.updateName('reached child change').auditAs('edit selected item'); + unrelated.updateName('UNREACHED CHILD MUST REMAIN PENDING'); + receiver.orderItemList().splice(0, receiver.orderItemList().length, reached); + receiver.updateDescription('receiver own change'); + reset(); + await receiver.auditAs('save scoped graph').save(context); + assert.equal(client.commands.length, 2); + assert.deepEqual(client.commands.map(command => command.entity), ['CustomerOrder', 'OrderItem']); + assert.equal(ledger(receiver).change(key('OrderItem', unrelated.id)).name, undefined, 'do not import an unreachable key'); + assert.equal(sourceRoot.change(key('CustomerOrder', source.id)).description, 'FOREIGN ROOT MUST REMAIN PENDING'); + assert.equal(sourceRoot.change(key('OrderItem', unrelated.id)).name, 'UNREACHED CHILD MUST REMAIN PENDING'); + assert.equal(sourceRoot.change(key('OrderItem', reached.id)).name, 'reached child change', 'import does not clear source'); + const expected = [[['auditReason', 'CustomerOrder', receiver.id, 'save scoped graph']], + [['auditReason', 'CustomerOrder', receiver.id, 'save scoped graph'], ['auditReason', 'OrderItem', reached.id, 'edit selected item']]]; + assert.deepEqual(client.commands.map(command => plain(command.lineage)), expected); + assert.deepEqual(audit.map(event => plain(event.mutationLineage)), expected); + const reread = await load(receiver.id!); + observe('scoped'); + assert.equal(E.customerOrder(reread).description().eval(), 'receiver own change'); + const byId = new Map(reread.orderItemList().map(item => [item.id, item])); + assert.equal(E.orderItem(byId.get(reached.id)!).name().eval(), 'reached child change'); + assert.notEqual(E.orderItem(byId.get(unrelated.id)!).name().eval(), 'UNREACHED CHILD MUST REMAIN PENDING'); + checks.push('scoped'); + console.log('SCOPED IMPORT PASSED: mutations=2 audits=2 foreign_root_retained=true unreachable_child_retained=true'); + } + if (!scenario || scenario === 'descendant') { + const created = await create('descendant'), loaded = await load(created.id!); + const version = E.customerOrder(loaded).version().eval(); + loaded.orderItemList()[0].updateName('only the child changed').auditAs('edit child under clean parent'); + reset(); + await loaded.auditAs('save descendant change').save(context); + assert.deepEqual(client.commands.map(command => command.entity), ['OrderItem']); + assert.equal(E.customerOrder(loaded).version().eval(), version); + assert.deepEqual(plain(audit[0].mutationLineage), [ + ['auditReason', 'CustomerOrder', loaded.id, 'save descendant change'], + ['auditReason', 'OrderItem', loaded.orderItemList()[0].id, 'edit child under clean parent'], + ]); + const reread = await load(loaded.id!); + assert.equal(E.orderItem(reread.orderItemList()[0]).name().eval(), 'only the child changed'); + assert.equal(E.customerOrder(reread).version().eval(), version); + checks.push('descendant'); + observe('descendant'); + console.log('CLEAN ANCESTOR PASSED: mutations=1 audits=1 parent_version_unchanged=true lineage_inherited=true'); + } + if (!scenario || scenario === 'conflict') { + const created = await create('conflict'); + const old = await load(created.id!); + const newer = await load(created.id!); + await newer.orderItemList()[0].updateName('committed newer version').auditAs('advance selected item').save(context); + const fresh = await load(created.id!); + const conflicting = fresh.orderItemList()[0].updateName('uncommitted conflicting version'); + const pendingSource = ledger(conflicting); + old.orderItemList().push(conflicting); + old.updateDescription('pending receiver must survive conflict'); + reset(); + await assert.rejects(old.auditAs('reject mixed versions').save(context), /ENTITY_VERSION_CONFLICT/); + assert.equal(client.commands.length, 0); assert.equal(audit.length, 0); + assert.equal(sql.snapshot().filter(entry => ['insert', 'update', 'delete'].includes(entry.operation)).length, 0); + assert.equal(ledger(old).change(key('CustomerOrder', old.id)).description, 'pending receiver must survive conflict'); + assert.equal(pendingSource.change(key('OrderItem', conflicting.id)).name, 'uncommitted conflicting version'); + checks.push('conflict'); + observe('conflict'); + console.log('MIXED VERSION REJECTED: mutations=0 audits=0 receiver_pending=true source_pending=true'); + } + } finally { await client.close(); } + assert.equal(manifest('lib'), before, 'acceptance must not edit generated source'); + mkdirSync('evidence', { recursive: true }); + writeFileSync('evidence/shared-reference-library.sha256', before); + console.log(JSON.stringify({ database, nonce, checks, generatedSourceUnchanged: true })); + console.log(`PASS TypeScript shared-reference example: ${checks.length} checks`); +} +main().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/source/teaql-ts/examples/trace-chain/standard-export-responses.ts b/source/teaql-ts/examples/trace-chain/standard-export-responses.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b3cde7b0ec7483ba6a4b987199832fd8ceafa2f --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/standard-export-responses.ts @@ -0,0 +1,142 @@ +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; +import { UserContext } from 'teaql-ts'; +import { SQLiteTeaQLClient } from 'teaql-ts/sql/sqlite'; +import { SQLExecutionEvidenceStore } from 'teaql-ts/sql/core'; +import { Q } from './lib/src/generated/Q'; +import { E } from './lib/src/generated/E'; +import { GENERATED_RUNTIME_MODULE } from './lib/src/runtime-module'; +import { startStandardNodeTelemetry } from 'teaql-ts/telemetry/standard-node'; + +// Application-owned fault boundary: no SDK setup or business-operation instrumentation. +class Client extends SQLiteTeaQLClient { + failSecondItem = false; + private itemCalls = 0; + readonly failure = new Error('controlled business failure'); + async executeMutation(source: any): Promise { + if (this.failSecondItem && (source.mutation ?? source).entity === 'OrderItem' && ++this.itemCalls === 2) + throw this.failure; + return super.executeMutation(source); + } +} + +async function main() { + const pressure = process.argv.includes('--queue-pressure'); + const backlog = process.argv.includes('--shutdown-backlog'); + const handle = pressure ? await startStandardNodeTelemetry() : undefined; + if (pressure) assert(handle?.health, 'pressure verification requires the standard module enabled'); + const client = new Client(process.env.TEAQL_STANDARD_RESPONSE_DB!, GENERATED_RUNTIME_MODULE.schemas) + .install(GENERATED_RUNTIME_MODULE).setDiagnosticSQLLogSink(undefined); + client.setQueryLoggingEnabled(false).setMutationLoggingEnabled(false); + const context = new UserContext().insertResource('dataService', client); + if (handle) client.setRuntimeTelemetry(handle.telemetry); + client.setUserContext(context); + const physical = new SQLExecutionEvidenceStore(); client.setRuntimeTelemetrySink(physical); + const audits: Readonly>[] = []; client.setAuditSink(event => {audits.push(event);}); + const group = 'response-' + randomUUID(); + const graph = () => { + const order = Q.customerOrders().comment('prepare governed response graph').purpose('verify exporter isolation') + .newEntity(context).updatePlatform('1').updateOrderNumber(group).updateDescription(group); + order.orderItemList().push( + Q.orderItems().comment('prepare inherited response item').purpose('verify exporter isolation') + .newEntity(context).updateName('TEAQL_PRIVATE_ITEM_LEFT'), + Q.orderItems().comment('prepare explicit response item').purpose('verify exporter isolation') + .newEntity(context).updateName('TEAQL_PRIVATE_ITEM_RIGHT').auditAs('prepare explicit item'), + ); + return order; + }; + try { + await context.ensureSchema(); const firstSeedAudits = audits.length; + await context.ensureSchema(); assert.equal(audits.length, firstSeedAudits); + audits.length = 0; physical.enableAll(); + const initial = graph(); await initial.auditAs('save first governed graph').save(context); + assert.equal(audits.length, 3); + const recovering = graph(); client.failSecondItem = true; + const beforeFailure = audits.length; + await assert.rejects(recovering.auditAs('reject interrupted graph').save(context), error => error === client.failure); + assert.equal(audits.length, beforeFailure, 'rolled-back writes must not emit committed audit'); + assert.equal(recovering.id, undefined, 'rollback restores the new-entity state'); + client.failSecondItem = false; + const rejectedRows = await Q.customerOrders().withDescriptionIs(group).limit(10) + .comment('inspect rolled back graph').purpose('verify rollback isolation').executeForList(context); + assert.equal(rejectedRows.length, 1); + await recovering.auditAs('recover interrupted graph').save(context); + assert.equal(audits.length, 6); + const rows = await Q.customerOrders().withIdIs(recovering.id) + .selectOrderItemListWith(Q.orderItems().limit(3)).limit(1) + .comment('inspect recovered graph').purpose('verify exporter isolation').executeForList(context); + assert.equal(rows.length, 1); + assert.equal(E.customerOrder(rows[0]).orderItemList().size().eval(), 2); + const loaded = rows[0]; + loaded.updateDescription(group + '-updated'); loaded.orderItemList()[0].updateName('TEAQL_PRIVATE_ITEM_UPDATED'); + await loaded.auditAs('update selected graph item').save(context); + assert.equal(audits.length, 8, 'unchanged second child must not be written'); + loaded.orderItemList()[1].markForDeletion(); + await loaded.auditAs('remove second graph item').save(context); + assert.equal(audits.length, 9, 'unchanged root and first child must not be written'); + const final = await Q.customerOrders().withIdIs(loaded.id) + .selectOrderItemListWith(Q.orderItems().limit(3)).limit(1) + .comment('inspect final graph').purpose('verify exporter isolation').executeForList(context); + assert.equal(E.customerOrder(final[0]).description().eval(), group + '-updated'); + assert.equal(E.customerOrder(final[0]).orderItemList().size().eval(), 1); + assert.equal(E.orderItem(final[0].orderItemList()[0]).name().eval(), 'TEAQL_PRIVATE_ITEM_UPDATED'); + assert(!JSON.stringify(physical.snapshot()).includes('TEAQL_PRIVATE_ITEM_')); + if (!pressure) await client.flushTelemetry(); + console.log('STANDARD_RESPONSE_EXAMPLE ' + JSON.stringify({firstSeedAudits, repeatSeedAudits: 0, + savedEntities: 3, rolledBackEntities: 3, recoveredEntities: 3, committedAudits: audits.length, + updatedEntities: 2, deletedEntities: 1, generatedQandE: true, originalFailurePreserved: true, + SQL: physical.snapshot().length, ordinaryLogging: false, applicationSDKInitialization: false})); + console.log('PASS TypeScript standard response graph: create, rollback, retry, update, delete, Q/E, committed audit'); + if (handle) { + await handle.flush(); + const baseline = handle.health!(); + const baselineAudits = audits.length; + for (let index = 0; index < 1500; index++) { + const read = await Q.customerOrders().withIdIs(loaded.id).limit(1) + .comment('read graph under telemetry pressure').purpose('verify exact bounded admission').executeForList(context); + assert.equal(E.customerOrder(read[0]).description().eval(), group + '-updated'); + } + assert.equal(audits.length, baselineAudits); + const saturated = handle.health!(); + for (const signal of ['traces','logs'] as const) { + const queue = saturated.queues[signal]; + assert.equal(queue.outstanding, 2048); + assert(queue.overflow > baseline.queues[signal].overflow); + assert.equal(queue.admitted - baseline.queues[signal].admitted, 2048); + } + console.log('TS_PRESSURE_READY ' + JSON.stringify({baseline,saturated,reads:1500,audits:baselineAudits})); + if (backlog) { + const started = performance.now(); + await handle.release(); + const closed = handle.health!(); + assert(performance.now() - started < 6500); + for (const signal of ['traces','logs'] as const) { + const queue = closed.queues[signal]; + assert.equal(queue.outstanding,0); assert.equal(queue.exporting,0); + assert.equal(queue.admitted,queue.completed+queue.retired); + } + assert(closed.queues.logs.retired > 0); + const late = await Q.customerOrders().withIdIs(loaded.id).limit(1) + .comment('read after telemetry shutdown').purpose('verify business continuity').executeForList(context); + assert.equal(E.customerOrder(late[0]).description().eval(),group+'-updated'); + assert.equal(audits.length,baselineAudits); + const after = handle.health!(); + for (const signal of ['traces','logs'] as const) + assert.equal(after.queues[signal].postShutdown-closed.queues[signal].postShutdown,3); + console.log('TS_BACKLOG_CLOSED '+JSON.stringify({elapsedMs:performance.now()-started,closed,after,audits:audits.length})); + console.log('PASS TypeScript standard backlogged shutdown and post-shutdown Q/E'); + } else { + await handle.flush(); const drained = handle.health!(); + for (const signal of ['traces','logs'] as const) assert.equal(drained.queues[signal].outstanding,0); + await Q.customerOrders().withIdIs(loaded.id).limit(1).comment('recovery after telemetry pressure') + .purpose('verify released capacity').executeForList(context); + await handle.flush(); const recovered = handle.health!(); + assert.equal(recovered.queues.traces.admitted-drained.queues.traces.admitted,3); + assert.equal(recovered.queues.logs.admitted-drained.queues.logs.admitted,4); + console.log('TS_PRESSURE_RECOVERED '+JSON.stringify({drained,recovered,audits:audits.length})); + console.log('PASS TypeScript standard queue pressure and recovery'); + } + } + } finally {try {await handle?.release();} finally {await client.close();}} +} +main().catch(error => {console.error(error); process.exitCode = 1;}); diff --git a/source/teaql-ts/examples/trace-chain/standard-telemetry-graph.ts b/source/teaql-ts/examples/trace-chain/standard-telemetry-graph.ts new file mode 100644 index 0000000000000000000000000000000000000000..3ccc307676e9ead71df2804895146a3e9fba5b15 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/standard-telemetry-graph.ts @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { UserContext } from 'teaql-ts'; +import { SQLiteTeaQLClient } from 'teaql-ts/sql/sqlite'; +import { SQLExecutionEvidenceStore } from 'teaql-ts/sql/core'; +import { Q } from './lib/src/generated/Q'; +import { E } from './lib/src/generated/E'; +import { GENERATED_RUNTIME_MODULE } from './lib/src/runtime-module'; + +async function main() { + const directory = mkdtempSync(join(tmpdir(), 'teaql-standard-graph-')); + const client = new SQLiteTeaQLClient(join(directory, 'graph.sqlite'), GENERATED_RUNTIME_MODULE.schemas) + .install(GENERATED_RUNTIME_MODULE); + const context = new UserContext().insertResource('dataService', client); + client.setUserContext(context).setDiagnosticSQLLogSink(undefined); + const physical = new SQLExecutionEvidenceStore(); client.setRuntimeTelemetrySink(physical); + const audits: Readonly>[] = []; client.setAuditSink(event => { audits.push(event); }); + try { + await context.ensureSchema(); + physical.enableAll(); audits.length = 0; + const order = Q.customerOrders().comment('prepare graph root').purpose('verify standard graph telemetry') + .newEntity(context).updatePlatform('1').updateOrderNumber('STANDARD-GRAPH').updateDescription('fixture'); + const inherited = Q.orderItems().comment('prepare inherited item').purpose('verify standard graph telemetry').newEntity(context) + .updateName('STANDARD_GRAPH_PRIVATE_LEFT'); + const explicit = Q.orderItems().comment('prepare explicit item').purpose('verify standard graph telemetry').newEntity(context) + .updateName('STANDARD_GRAPH_PRIVATE_RIGHT').auditAs('prepare second graph item'); + order.orderItemList().push(inherited, explicit); + await order.auditAs('save governed graph').save(context); + assert.equal(audits.length, 3); + assert.equal(physical.snapshot().length, 6); + const loaded = await Q.customerOrders().withIdIs(order.id) + .selectOrderItemListWith(Q.orderItems().limit(3)).limit(1) + .comment('inspect saved graph').purpose('verify standard graph telemetry').executeForList(context); + assert.equal(loaded.length, 1); + assert.equal(E.customerOrder(loaded[0]).orderItemList().size().eval(), 2); + const lineages = physical.snapshot().filter(entry => entry.operation === 'insert') + .map(entry => entry.mutationLineage!.map(node => node.detail)); + assert.deepEqual(lineages, [['save governed graph'], ['save governed graph'], + ['save governed graph', 'prepare second graph item']]); + assert(!JSON.stringify(physical.snapshot()).includes('STANDARD_GRAPH_PRIVATE_')); + await client.flushTelemetry(); + console.log('STANDARD_GRAPH_EXAMPLE ' + JSON.stringify({ sqlExecutions: 6, committedAudits: audits.length, + inheritedLineage: true, explicitLineage: true, generatedQandE: true, applicationSDKInitialization: false })); + console.log('PASS TypeScript standard graph telemetry: one graph, three entities, six SQL, independent committed audit'); + } finally { await client.close(); } +} +main().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/source/teaql-ts/examples/trace-chain/standard-telemetry.ts b/source/teaql-ts/examples/trace-chain/standard-telemetry.ts new file mode 100644 index 0000000000000000000000000000000000000000..b71b1bd4fd5688d58080c6dcabf0603591f92bc3 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/standard-telemetry.ts @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { UserContext } from 'teaql-ts'; +import { SQLiteTeaQLClient } from 'teaql-ts/sql/sqlite'; +import { SQLExecutionEvidenceStore } from 'teaql-ts/sql/core'; +import { Q } from './lib/src/generated/Q'; +import { E } from './lib/src/generated/E'; +import { GENERATED_RUNTIME_MODULE } from './lib/src/runtime-module'; + +async function main(): Promise { + const directory = mkdtempSync(join(tmpdir(), 'teaql-standard-telemetry-')); + // No OpenTelemetry imports, SDK initialization or per-query instrumentation. + const client = new SQLiteTeaQLClient(join(directory, 'startup.sqlite'), GENERATED_RUNTIME_MODULE.schemas) + .install(GENERATED_RUNTIME_MODULE); + const context = new UserContext().insertResource('dataService', client); + const evidence = new SQLExecutionEvidenceStore(); + let committedAudits = 0; + client.setRuntimeTelemetrySink(evidence).setAuditSink(() => { committedAudits++; }); + try { + await context.ensureSchema(); + assert.equal(committedAudits, 1, 'the fresh generated root must be seeded through audited mutation'); + await context.ensureSchema(); + assert.equal(committedAudits, 1, 'repeated startup must not repeat the root mutation'); + for (let run = 0; run < 2; run++) { + const roots = await Q.platforms().withIdIs('1').limit(1) + .comment('inspect seeded root').purpose('verify standard telemetry startup').executeForList(context); + assert.equal(roots.length, 1); + assert.equal(E.platform(roots[0]).id().eval(), '1'); + } + assert.equal(evidence.snapshot().length, 6, 'seed lookup/write/readback, repeat lookup and two bounded Q requests'); + assert(evidence.snapshot().every(entry => entry.logMode === 'masked')); + await client.flushTelemetry(); + const sdkModules = Object.keys(require.cache).filter(file => + /node_modules\/@opentelemetry\/(sdk-|exporter-|context-async-hooks)/.test(file)); + if (process.env.TEAQL_TELEMETRY_ENABLED !== 'true') { + assert.equal(sdkModules.length, 0, 'disabled startup must not even load the SDK/exporters'); + } else assert(sdkModules.length > 0, 'enabled startup must load the standard SDK module'); + console.log('STANDARD_TELEMETRY_EXAMPLE ' + JSON.stringify({ + enabled: process.env.TEAQL_TELEMETRY_ENABLED === 'true', + sqlExecutions: evidence.snapshot().length, seedAudits: committedAudits, + repeatedSeedWrites: 0, generatedQandE: true, sdkInitializationInApplication: false, + sdkModulesLoaded: sdkModules.length, + })); + console.log('PASS TypeScript standard telemetry: startup, audited seed, repeated seed, generated Q/E and safe SQL'); + } finally { await client.close(); } +} +main().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/source/teaql-ts/examples/trace-chain/stream-capture.ts b/source/teaql-ts/examples/trace-chain/stream-capture.ts new file mode 100644 index 0000000000000000000000000000000000000000..afb04f35d37b469338e87002808df651478e8558 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/stream-capture.ts @@ -0,0 +1,98 @@ +import assert from 'node:assert/strict'; +import {randomUUID,createHash} from 'node:crypto'; +import {mkdirSync,readFileSync,readdirSync} from 'node:fs'; +import {dirname,join,resolve} from 'node:path'; +import {EntityRoot,SelectQuery,UserContext} from 'teaql-ts'; +import {AbstractSQLTeaQLClient,SQLExecutionEvidenceStore} from 'teaql-ts/sql/core'; +import {SQLiteDriver} from 'teaql-ts/sql/sqlite'; +import {Q} from './lib/src/generated/Q'; +import {E} from './lib/src/generated/E'; +import {GENERATED_RUNTIME_MODULE} from './lib/src/runtime-module'; + +const manifest=(directory:string):string=>readdirSync(directory,{withFileTypes:true}) + .sort((a,b)=>a.name.localeCompare(b.name)).map(entry=>entry.isDirectory() + ?manifest(join(directory,entry.name)):createHash('sha256').update(readFileSync(join(directory,entry.name))).digest('hex')).join(''); +class Client extends AbstractSQLTeaQLClient { + constructor(driver:SQLiteDriver){super(driver,GENERATED_RUNTIME_MODULE.schemas);} +} +class Context extends UserContext { + allowed:string[]=[]; + calls=0; + override prepareQuery(query:SelectQuery):SelectQuery { + this.calls++; + const prepared=super.prepareQuery(query); + return query.entity==='CustomerOrder' + ?prepared.filter({$and:[query.filterCondition,{id:{$in:this.allowed}}]}):prepared; + } +} +async function collect(stream:AsyncIterable):Promise { + const rows:T[]=[];for await(const row of stream)rows.push(row);return rows; +} +const ledger=(entity:unknown)=>(entity as {_root:EntityRoot})._root; +async function main(){ + const before=manifest('lib'); + const path=resolve(process.env.TEAQL_TRACE_CHAIN_STREAM_DB??'.local/stream-capture.sqlite'); + mkdirSync(dirname(path),{recursive:true}); + const client=new Client(new SQLiteDriver(path)).install(GENERATED_RUNTIME_MODULE).setDiagnosticSQLLogSink(undefined); + const context=new Context().insertResource('dataService',client); + const sql=new SQLExecutionEvidenceStore();client.setRuntimeTelemetrySink(sql); + const group=`STREAM-${randomUUID()}`,secret='PRIVATE-STREAM-CHILD'; + const ids:string[]=[]; + try{ + await context.ensureSchema();await context.ensureSchema(); + for(let index=0;index<3;index++){ + const root=Q.customerOrders().comment('seed stream capture').purpose('verify delayed consumption').newEntity(context) + .updatePlatform('1').updateOrderNumber(`${group}-${index}`).updateDescription(group); + root.orderItemList().push(Q.orderItems().comment('seed stream child').purpose('verify delayed relation capture') + .newEntity(context).updateName(secret)); + await root.auditAs('seed delayed stream graph').save(context);ids.push(root.id!); + } + sql.enableAll();context.calls=0;context.allowed=ids.slice(0,2); + const child=Q.orderItems().withNameIs(secret).limit(2); + const builder=Q.customerOrders().withIdIn(...ids).selectOrderItemListWith(child).orderByIdAscending().limit(3); + const request=builder.comment(`first ${secret} ${group}`).purpose('first stream'); + const first=request.executeForStream(context,1); + assert.equal(context.calls,1,'Context preparation belongs to stream creation, not first poll'); + assert.equal(sql.snapshot().length,0,'creating a stream does not execute SQL'); + request.comment('caller replaced first intent');child.withNameIs('no match'); + context.allowed=ids.slice(2); + const second=Q.customerOrders().withIdIn(...ids) + .selectOrderItemListWith(Q.orderItems().withNameIs(secret).limit(2)).limit(3) + .comment(`second ${secret} ${group}`).purpose('second stream').executeForStream(context,1); + assert.equal(context.calls,2);context.allowed=[]; + const [a,b]=await Promise.all([collect(first),collect(second)]); + assert.deepEqual(a.map(row=>E.customerOrder(row).id().eval()),ids.slice(0,2)); + assert.deepEqual(b.map(row=>E.customerOrder(row).id().eval()),ids.slice(2)); + assert.equal(new Set([...a,...b].map(ledger)).size,3); + for(const row of [...a,...b]){ + assert.equal(row.orderItemList().length,1); + assert.equal(E.orderItem(row.orderItemList()[0]).name().eval(),secret); + assert.equal(ledger(row),ledger(row.orderItemList()[0])); + } + const observations=sql.snapshot(); + assert.equal(observations.length,5,'two root SELECTs and three real child probes'); + assert(!JSON.stringify(observations).includes(secret)); + assert(!JSON.stringify(observations).includes('caller replaced')); + for(const entry of observations){ + assert(entry.comment?.includes(group)); + assert(['first stream','second stream'].includes(entry.purpose!)); + assert.equal(entry.tracePath[0].name,'CustomerOrder'); + assert.equal(entry.tracePath[entry.tracePath.length-1].kind,'sql'); + } + sql.enableAll();context.allowed=ids; + const cancelled=Q.customerOrders().withIdIn(...ids).orderByIdAscending().limit(3) + .comment('cancel after one').purpose('verify stream terminal').executeForStream(context,1)[Symbol.asyncIterator](); + try {assert.equal((await cancelled.next()).done,false);} finally {await cancelled.return?.();} + const terminal=sql.snapshot();assert.equal(terminal.length,1); + assert.equal(terminal[0].executionOutcome,'cancelled');assert.equal(terminal[0].resultCount,1); + sql.enableAll(); + const abandoned=Q.customerOrders().withIdIn(...ids).limit(3) + .comment('never consumed').purpose('verify no fake SQL').executeForStream(context,1)[Symbol.asyncIterator](); + await abandoned.return?.();assert.equal(sql.snapshot().length,0); + assert.equal(manifest('lib'),before,'generated source unchanged'); + console.log(`STREAM_CAPTURE_OBSERVED ${JSON.stringify({group,ids,first:a.length,second:b.length,physicalQueries:observations.length,cancelledRows:terminal[0].resultCount,neverPolledStatements:0})}`); + console.log('PASS TypeScript generated creation-time stream capture'); + }catch(error){console.error('STREAM_CAPTURE_SQL_SHAPES',client.sqlTrace.slice(-5));throw error;} + finally{await client.close();} +} +main().catch(error=>{console.error(error);process.exitCode=1;}); diff --git a/source/teaql-ts/examples/trace-chain/tsconfig.json b/source/teaql-ts/examples/trace-chain/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..a03a17e740216f9511d4ebd2b0f3e4d0e76420a9 --- /dev/null +++ b/source/teaql-ts/examples/trace-chain/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { "target": "ES2020", "module": "CommonJS", "moduleResolution": "node", + "strict": true, "esModuleInterop": true, "skipLibCheck": true }, + "include": ["app.ts", "shared-reference.ts", "paging.ts", "stream-capture.ts", "relation-aggregate.ts", "aggregate-membership.ts", "checker-overlap.ts", "plan-item-lineage.ts", "bootstrap-intent.ts", "standard-telemetry.ts", "standard-telemetry-graph.ts", "standard-export-responses.ts", "lib/src/**/*.ts"] +} diff --git a/source/teaql-ts/package-lock.json b/source/teaql-ts/package-lock.json index b6ae55c641fbd9cc6ad7754b83cf3ab818915f00..b753122b98c57569f77c2b95ff6ba017aea7d9eb 100644 --- a/source/teaql-ts/package-lock.json +++ b/source/teaql-ts/package-lock.json @@ -12,6 +12,8 @@ "node-fetch": "^2.7.0" }, "devDependencies": { + "@aws-sdk/client-s3": "3.1151.0", + "@aws-sdk/s3-request-presigner": "3.1151.0", "@opentelemetry/api-logs": "^0.221.0", "@opentelemetry/context-async-hooks": "^2.10.0", "@opentelemetry/core": "^2.10.0", @@ -40,6 +42,8 @@ "typescript": "5.4.5" }, "peerDependencies": { + "@aws-sdk/client-s3": "3.1151.0", + "@aws-sdk/s3-request-presigner": "3.1151.0", "@opentelemetry/api": "^1.9.1", "@opentelemetry/api-logs": "^0.221.0", "@opentelemetry/context-async-hooks": "^2.10.0", @@ -60,6 +64,12 @@ "pg-query-stream": "^4.7.0" }, "peerDependenciesMeta": { + "@aws-sdk/client-s3": { + "optional": true + }, + "@aws-sdk/s3-request-presigner": { + "optional": true + }, "@opentelemetry/api": { "optional": true }, @@ -116,6 +126,350 @@ } } }, + "node_modules/@aws-sdk/checksums": { + "version": "3.1001.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.3.tgz", + "integrity": "sha512-CcAkVyEUKGngbEqJdVdSc53Iy52+XzVL5NIcTen2BlK8UByNC1DThVtf5VKSrNzyA+SqyTvs+3S+knVBbOUizQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1151.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1151.0.tgz", + "integrity": "sha512-Kg20OP7F+rrWMFjPv/aCsNUt/qDbvh1bLJFQM8k/8fnxowR/DA67qL5KHzWWN5gwJCYplS3KQde6P/knOB+lxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/checksums": "^3.1001.3", + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/credential-provider-node": "^3.972.86", + "@aws-sdk/middleware-sdk-s3": "^3.972.79", + "@aws-sdk/signature-v4-multi-region": "^3.996.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.978.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.3.tgz", + "integrity": "sha512-nslpzUCTI9b2IQMWSd6mSAXldBexPc8rr0d3dXUutXYFLFaTdsHa2GD7sJdsn3PhyjK0O5K8ypd6zi+RXXPVlg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@aws-sdk/xml-builder": "^3.972.41", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.35.2", + "@smithy/signature-v4": "^5.7.4", + "@smithy/types": "^4.19.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.74", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.74.tgz", + "integrity": "sha512-rsRJcW2iFUhCgclyD6OSbMEaPl8Eu0qSeRZb2uuCK1ViZ66hl5wOe/p6Vr6MO3b+ydyKgFeqza2p6tZThvILlw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.76", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.76.tgz", + "integrity": "sha512-pnKdh0Br7OJPj+7qKjzgTfDTFW0RhEMKXNDR2jv9CAM4oHuG/4z3hRh8KtutLfpmItuRtDbKBmwWkDqHwwAWXw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.19.tgz", + "integrity": "sha512-kcjvAxqQEV8yuwkX58RelNn3ne65o7JgRWf3Z/gVvbi7Xuw1qSDLJlatrLD76+40cmRxmNkLxBoyp/vAQChQ5g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/credential-provider-env": "^3.972.74", + "@aws-sdk/credential-provider-http": "^3.972.76", + "@aws-sdk/credential-provider-login": "^3.972.81", + "@aws-sdk/credential-provider-process": "^3.972.74", + "@aws-sdk/credential-provider-sso": "^3.973.18", + "@aws-sdk/credential-provider-web-identity": "^3.972.80", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.81", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.81.tgz", + "integrity": "sha512-CeQHSbE2CnVdaHIJ/NayRaCecIER+Pm9faUPkmRATO54rCoMTEPSbVieGHzEwvm1dZM2HIs6qQZCznhayNTWJQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.86", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.86.tgz", + "integrity": "sha512-WNttSyr6e8innPtBSty//CdnqJcgZN6eAbtw3v/CR2IObz6MYZEo8eusdtuCHJ2ujwFqC2bSPHVyhXGHx8HruQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.74", + "@aws-sdk/credential-provider-http": "^3.972.76", + "@aws-sdk/credential-provider-ini": "^3.973.19", + "@aws-sdk/credential-provider-process": "^3.972.74", + "@aws-sdk/credential-provider-sso": "^3.973.18", + "@aws-sdk/credential-provider-web-identity": "^3.972.80", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.74", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.74.tgz", + "integrity": "sha512-Yj6kZFJQdbR+PygTkqdVLPHHpdDpOS9GCYVcDCuweCRRig9LF24ELQBMfyUw1vspvrZJUfnOGVyMjhaL094ahw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.18.tgz", + "integrity": "sha512-FnpPhdY4qUGtX/8X6eKxh+uD+9Y19i9FYibgk4SBe0M935s9vocHkHzaxaMNk+YkHmvzgdu8cHA5SCoeU+kkew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/token-providers": "3.1150.0", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.80", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.80.tgz", + "integrity": "sha512-TJF25Ndwy2HxcVj6oxMgp8qVlJAawlExHYBXqhuC6Zt2m+A7WpdkjBD6QglNbiArlteYljZHEY3R7KKmno6ISA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.79", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.79.tgz", + "integrity": "sha512-UfgAQosf4IEXldMsyntVuHsJnaQzlH6pmkOr6dQy7CaOYSlGz3mWiW4Di7XfQ/kPJP6xE7oMshz1V0E8RqGdRg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/signature-v4-multi-region": "^3.996.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.48.tgz", + "integrity": "sha512-B01jdXJtAW/XOiUzGdI2mz67WyEHbGEc9X6Euzw3bjLAa+txTqdZDER4X2FsIEcXzV2ABqudVZ5thali8tCCzQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/signature-v4-multi-region": "^3.996.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.1151.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1151.0.tgz", + "integrity": "sha512-RukZentLCvhlhpqQxv1LfONz/SkDeNQ/SOie5RFLI9vFKNB89VNUuGo0YVM9XdzMZ0GPoqWaiHg4ZNxbMpAjtQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/signature-v4-multi-region": "^3.996.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.48.tgz", + "integrity": "sha512-cFDXfEbXaGwbnpvR/DyqfxfjxpjXVRVthQLgkwvcfDS5u3+8G9WSj1X1Ag/pTRg/QOeOno1Tq3ycEdrKsCUcOA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@smithy/signature-v4": "^5.7.4", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1150.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1150.0.tgz", + "integrity": "sha512-wDU36ofhabZYu+d2KSriyDbwzxNoaOp+4dG0p3qkabk7HeALR5nIWyQbjoyJZLn3iqWkv/xNdyUfbfevDVDzzg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.974.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.6.tgz", + "integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz", + "integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -2228,6 +2582,93 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@smithy/core": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.36.0.tgz", + "integrity": "sha512-3hJiwG1F5t1SoXqp/yOCmYXziyx7ZVqF63S5W6YU201e/qwVaHPex+E/3vvyY9a3ORq/b+BydeNZpP2saQ9X2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", + "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz", + "integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz", + "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@sqlite.org/sqlite-wasm": { "version": "3.53.0-build1", "resolved": "https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.53.0-build1.tgz", @@ -2578,6 +3019,13 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "dev": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", @@ -7010,6 +7458,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -7260,6 +7715,274 @@ } }, "dependencies": { + "@aws-sdk/checksums": { + "version": "3.1001.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.3.tgz", + "integrity": "sha512-CcAkVyEUKGngbEqJdVdSc53Iy52+XzVL5NIcTen2BlK8UByNC1DThVtf5VKSrNzyA+SqyTvs+3S+knVBbOUizQ==", + "dev": true, + "requires": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/client-s3": { + "version": "3.1151.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1151.0.tgz", + "integrity": "sha512-Kg20OP7F+rrWMFjPv/aCsNUt/qDbvh1bLJFQM8k/8fnxowR/DA67qL5KHzWWN5gwJCYplS3KQde6P/knOB+lxg==", + "dev": true, + "requires": { + "@aws-sdk/checksums": "^3.1001.3", + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/credential-provider-node": "^3.972.86", + "@aws-sdk/middleware-sdk-s3": "^3.972.79", + "@aws-sdk/signature-v4-multi-region": "^3.996.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/core": { + "version": "3.978.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.3.tgz", + "integrity": "sha512-nslpzUCTI9b2IQMWSd6mSAXldBexPc8rr0d3dXUutXYFLFaTdsHa2GD7sJdsn3PhyjK0O5K8ypd6zi+RXXPVlg==", + "dev": true, + "requires": { + "@aws-sdk/types": "^3.974.6", + "@aws-sdk/xml-builder": "^3.972.41", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.35.2", + "@smithy/signature-v4": "^5.7.4", + "@smithy/types": "^4.19.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-env": { + "version": "3.972.74", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.74.tgz", + "integrity": "sha512-rsRJcW2iFUhCgclyD6OSbMEaPl8Eu0qSeRZb2uuCK1ViZ66hl5wOe/p6Vr6MO3b+ydyKgFeqza2p6tZThvILlw==", + "dev": true, + "requires": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-http": { + "version": "3.972.76", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.76.tgz", + "integrity": "sha512-pnKdh0Br7OJPj+7qKjzgTfDTFW0RhEMKXNDR2jv9CAM4oHuG/4z3hRh8KtutLfpmItuRtDbKBmwWkDqHwwAWXw==", + "dev": true, + "requires": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-ini": { + "version": "3.973.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.19.tgz", + "integrity": "sha512-kcjvAxqQEV8yuwkX58RelNn3ne65o7JgRWf3Z/gVvbi7Xuw1qSDLJlatrLD76+40cmRxmNkLxBoyp/vAQChQ5g==", + "dev": true, + "requires": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/credential-provider-env": "^3.972.74", + "@aws-sdk/credential-provider-http": "^3.972.76", + "@aws-sdk/credential-provider-login": "^3.972.81", + "@aws-sdk/credential-provider-process": "^3.972.74", + "@aws-sdk/credential-provider-sso": "^3.973.18", + "@aws-sdk/credential-provider-web-identity": "^3.972.80", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-login": { + "version": "3.972.81", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.81.tgz", + "integrity": "sha512-CeQHSbE2CnVdaHIJ/NayRaCecIER+Pm9faUPkmRATO54rCoMTEPSbVieGHzEwvm1dZM2HIs6qQZCznhayNTWJQ==", + "dev": true, + "requires": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-node": { + "version": "3.972.86", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.86.tgz", + "integrity": "sha512-WNttSyr6e8innPtBSty//CdnqJcgZN6eAbtw3v/CR2IObz6MYZEo8eusdtuCHJ2ujwFqC2bSPHVyhXGHx8HruQ==", + "dev": true, + "requires": { + "@aws-sdk/credential-provider-env": "^3.972.74", + "@aws-sdk/credential-provider-http": "^3.972.76", + "@aws-sdk/credential-provider-ini": "^3.973.19", + "@aws-sdk/credential-provider-process": "^3.972.74", + "@aws-sdk/credential-provider-sso": "^3.973.18", + "@aws-sdk/credential-provider-web-identity": "^3.972.80", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-process": { + "version": "3.972.74", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.74.tgz", + "integrity": "sha512-Yj6kZFJQdbR+PygTkqdVLPHHpdDpOS9GCYVcDCuweCRRig9LF24ELQBMfyUw1vspvrZJUfnOGVyMjhaL094ahw==", + "dev": true, + "requires": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-sso": { + "version": "3.973.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.18.tgz", + "integrity": "sha512-FnpPhdY4qUGtX/8X6eKxh+uD+9Y19i9FYibgk4SBe0M935s9vocHkHzaxaMNk+YkHmvzgdu8cHA5SCoeU+kkew==", + "dev": true, + "requires": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/token-providers": "3.1150.0", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-web-identity": { + "version": "3.972.80", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.80.tgz", + "integrity": "sha512-TJF25Ndwy2HxcVj6oxMgp8qVlJAawlExHYBXqhuC6Zt2m+A7WpdkjBD6QglNbiArlteYljZHEY3R7KKmno6ISA==", + "dev": true, + "requires": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/middleware-sdk-s3": { + "version": "3.972.79", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.79.tgz", + "integrity": "sha512-UfgAQosf4IEXldMsyntVuHsJnaQzlH6pmkOr6dQy7CaOYSlGz3mWiW4Di7XfQ/kPJP6xE7oMshz1V0E8RqGdRg==", + "dev": true, + "requires": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/signature-v4-multi-region": "^3.996.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/nested-clients": { + "version": "3.997.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.48.tgz", + "integrity": "sha512-B01jdXJtAW/XOiUzGdI2mz67WyEHbGEc9X6Euzw3bjLAa+txTqdZDER4X2FsIEcXzV2ABqudVZ5thali8tCCzQ==", + "dev": true, + "requires": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/signature-v4-multi-region": "^3.996.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/s3-request-presigner": { + "version": "3.1151.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1151.0.tgz", + "integrity": "sha512-RukZentLCvhlhpqQxv1LfONz/SkDeNQ/SOie5RFLI9vFKNB89VNUuGo0YVM9XdzMZ0GPoqWaiHg4ZNxbMpAjtQ==", + "dev": true, + "requires": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/signature-v4-multi-region": "^3.996.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/signature-v4-multi-region": { + "version": "3.996.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.48.tgz", + "integrity": "sha512-cFDXfEbXaGwbnpvR/DyqfxfjxpjXVRVthQLgkwvcfDS5u3+8G9WSj1X1Ag/pTRg/QOeOno1Tq3ycEdrKsCUcOA==", + "dev": true, + "requires": { + "@aws-sdk/types": "^3.974.6", + "@smithy/signature-v4": "^5.7.4", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/token-providers": { + "version": "3.1150.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1150.0.tgz", + "integrity": "sha512-wDU36ofhabZYu+d2KSriyDbwzxNoaOp+4dG0p3qkabk7HeALR5nIWyQbjoyJZLn3iqWkv/xNdyUfbfevDVDzzg==", + "dev": true, + "requires": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/types": { + "version": "3.974.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.6.tgz", + "integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==", + "dev": true, + "requires": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/xml-builder": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz", + "integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==", + "dev": true, + "requires": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "dev": true + }, "@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -8715,6 +9438,69 @@ "@sinonjs/commons": "^3.0.0" } }, + "@smithy/core": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.36.0.tgz", + "integrity": "sha512-3hJiwG1F5t1SoXqp/yOCmYXziyx7ZVqF63S5W6YU201e/qwVaHPex+E/3vvyY9a3ORq/b+BydeNZpP2saQ9X2g==", + "dev": true, + "requires": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "dev": true, + "requires": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + } + }, + "@smithy/fetch-http-handler": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", + "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", + "dev": true, + "requires": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + } + }, + "@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "dev": true, + "requires": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + } + }, + "@smithy/signature-v4": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz", + "integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==", + "dev": true, + "requires": { + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz", + "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==", + "dev": true, + "requires": { + "tslib": "^2.6.2" + } + }, "@sqlite.org/sqlite-wasm": { "version": "3.53.0-build1", "resolved": "https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.53.0-build1.tgz", @@ -8999,6 +9785,12 @@ "readable-stream": "^3.4.0" } }, + "bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "dev": true + }, "brace-expansion": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", @@ -12255,6 +13047,12 @@ } } }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/source/teaql-ts/package.json b/source/teaql-ts/package.json index 320d043cd9395b32877bc6389fc7f76d1e62d638..c764f8ee4ff53fdcdb0855eaa14385f81828238a 100644 --- a/source/teaql-ts/package.json +++ b/source/teaql-ts/package.json @@ -9,6 +9,11 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { + "./object-storage/s3": { + "types": "./dist/object-storage/s3.d.ts", + "import": "./dist/esm/object-storage/s3.js", + "require": "./dist/object-storage/s3.js" + }, ".": { "types": "./dist/index.d.ts", "import": "./dist/esm/index.js", @@ -72,6 +77,7 @@ }, "typesVersions": { "*": { + "object-storage/s3": ["dist/object-storage/s3.d.ts"], "sql/core": [ "dist/sql/core.d.ts" ], @@ -124,6 +130,8 @@ "access": "public" }, "devDependencies": { + "@aws-sdk/client-s3": "3.1151.0", + "@aws-sdk/s3-request-presigner": "3.1151.0", "@opentelemetry/api-logs": "^0.221.0", "@opentelemetry/context-async-hooks": "^2.10.0", "@opentelemetry/core": "^2.10.0", @@ -155,6 +163,8 @@ "node-fetch": "^2.7.0" }, "peerDependencies": { + "@aws-sdk/client-s3": "3.1151.0", + "@aws-sdk/s3-request-presigner": "3.1151.0", "@opentelemetry/otlp-transformer": "0.221.0", "@opentelemetry/core": "^2.10.0", "@opentelemetry/context-async-hooks": "^2.10.0", @@ -175,6 +185,8 @@ "pg-query-stream": "^4.7.0" }, "peerDependenciesMeta": { + "@aws-sdk/client-s3": { "optional": true }, + "@aws-sdk/s3-request-presigner": { "optional": true }, "@opentelemetry/otlp-transformer": { "optional": true }, "@opentelemetry/core": { "optional": true }, "@opentelemetry/context-async-hooks": { "optional": true }, diff --git a/source/teaql-ts/releases/0.2.7.md b/source/teaql-ts/releases/0.2.7.md new file mode 100644 index 0000000000000000000000000000000000000000..dee531ff518f984d91562319ebccc2bb542da230 --- /dev/null +++ b/source/teaql-ts/releases/0.2.7.md @@ -0,0 +1,34 @@ +# 0.2.7: default log privacy + +Tracking: https://github.com/teaql/teaql-ts/issues/34 +Cross-runtime contract: https://github.com/teaql/teaql-conformance/issues/58 + +## Behavior and migration + +Runtime-controlled SQL and audit logging now redact sensitive payloads before +normal, custom, buffered and file destinations. Execution values are unchanged. +Selecting a diagnostic sink alone no longer authorizes plaintext output. + +For intentional plaintext debugging only, set this exact acknowledgement: + +```sh +export TEAQL_ALLOW_SENSITIVE_PLAINTEXT_LOGS=I_UNDERSTAND_SENSITIVE_DATA_MAY_BE_WRITTEN_TO_DISK +``` + +Invalid, empty or truthy shortcut values do not enable it. Enabling it emits a +value-free warning. Credential-classified fields remain hidden. The switch grants +permission; it does not force every sink to reveal values. Restart after changing +the environment. Previously written files are not erased. + +Without reliable parameter/field provenance, SQL logs use conservative redaction; +SQL may be labeled non-replayable. Operations, timing and safe intent remain. +Do not put unrelated secrets in comments/purpose, or print raw business events. +Arbitrary application and third-party driver logs are outside this boundary. + +## Verification + +Local runtime tests and `bash scripts/verify-examples.sh` passed before release +preparation. Retained real SQLite CRUD/failure canaries assert unchanged persisted +values and absence of sensitive markers in runtime log destinations. Exact/invalid +environment gates and credential behavior have separate regression fixtures. +Published-package verification is a separate gate, not implied by local results. diff --git a/source/teaql-ts/releases/0.2.8.md b/source/teaql-ts/releases/0.2.8.md new file mode 100644 index 0000000000000000000000000000000000000000..f961733936ed903ba01301fcd49b628ac163a129 --- /dev/null +++ b/source/teaql-ts/releases/0.2.8.md @@ -0,0 +1,14 @@ +# 0.2.8: default log privacy + +Tracking: https://github.com/teaql/teaql-ts/issues/34 + +Includes the default log redaction and exact plaintext acknowledgement described +in [0.2.7](0.2.7.md), plus production-built browser smoke verification. + +The 0.2.7 publication stopped before npm publish: Jest's `toThrow` did not +recognize a rejected native SQLite exception in the hosted test environment. +The canary now asserts its exact `SQLITE_CONSTRAINT_PRIMARYKEY` code, retaining +the rejection requirement and unchanged-row assertion. The old tag is not moved. + +Local full suite: 141 passed, 2 skipped. Hosted release tests and public package +resolution must succeed before publication is reported complete. diff --git a/source/teaql-ts/releases/0.3.0.md b/source/teaql-ts/releases/0.3.0.md new file mode 100644 index 0000000000000000000000000000000000000000..8408451eff6ab092796ca403a9b294ea045a87c6 --- /dev/null +++ b/source/teaql-ts/releases/0.3.0.md @@ -0,0 +1,18 @@ +# TeaQL TypeScript 0.3.0 + +Tracking: [release issue 46](https://github.com/teaql/teaql-ts/issues/46). + +This release brings the merged request intent and Trace Chain implementation +to the public package. It includes query and mutation lineage, independent +graph ownership, relation aggregates, facet tracing and safe SQL diagnostics. +Generated-library examples cover shared references, partial relation loading, +Checker overlap, stream capture and repeat bootstrap behavior. + +Regenerate applications when upgrading from 0.2.x: request and lineage +contracts have changed. Logging remains enabled by default, with existing +disable controls and sensitive-parameter masking preserved. + +Release gates are the complete local example inventory, native tests, +an isolated consumer of the immutable internal 0.3.0 package, and public +package delivery checks. External-provider skips remain explicit; this +release does not claim complete TFP or every enterprise database support. diff --git a/source/teaql-ts/scripts/build-esm.mjs b/source/teaql-ts/scripts/build-esm.mjs index 6c917e105f2aec5815638eea1a827bd1a401df4b..6f3c88d4724dbed6309461b8dfc9743c08aaf356 100644 --- a/source/teaql-ts/scripts/build-esm.mjs +++ b/source/teaql-ts/scripts/build-esm.mjs @@ -6,6 +6,7 @@ await rm(output, {recursive: true, force: true}); await build({ entryPoints: { index: 'src/index.ts', + 'object-storage/s3': 'src/object-storage/s3.ts', 'sql/core': 'src/sql/core.ts', 'sql/postgres': 'src/sql/postgres.ts', 'sql/mysql': 'src/sql/mysql.ts', diff --git a/source/teaql-ts/scripts/verify-examples.sh b/source/teaql-ts/scripts/verify-examples.sh index 460849de084edc98aff022b906c0f1a2fa16e594..6a2880e3037ed0dbafdefb6c26d8f1715bf02427 100644 --- a/source/teaql-ts/scripts/verify-examples.sh +++ b/source/teaql-ts/scripts/verify-examples.sh @@ -5,9 +5,10 @@ repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # file: dependencies resolve the package's dist entry points, not TypeScript source. # Rebuild here so examples can never silently validate stale checked-in output. (cd "$repo" && npm run build) +bash "$repo/examples/object-storage/verify.sh" (cd "$repo" && npm test -- --runInBand tests/sql-relation-membership.test.ts) mkdir -p "$repo/.local" -expected=(browser-sqlite conformance ensure-schema-bootstrap facet-trace order-management school-management task-board trace-chain) +expected=(browser-sqlite conformance ensure-schema-bootstrap facet-trace object-storage order-management school-management task-board trace-chain) mapfile -t actual < <(find "$repo/examples" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort) if [[ "${actual[*]}" != "${expected[*]}" ]]; then echo "example inventory changed; update scripts/verify-examples.sh: ${actual[*]}" >&2 diff --git a/source/teaql-ts/src/core/context.ts b/source/teaql-ts/src/core/context.ts index 15ac9e7354957df97face00a8723b9860f79fdbf..12ac014ba58a3191f4d3ef06416f371d66d86438 100644 --- a/source/teaql-ts/src/core/context.ts +++ b/source/teaql-ts/src/core/context.ts @@ -1,4 +1,5 @@ import { SelectQuery } from './ast'; +import { ObjectStorage, ObjectStorageProvider, ObjectStorageObserver } from './object-storage'; import { CheckResult, I18nCatalog, Locale, parseLocale } from './i18n'; import { contextSchemaCapability, @@ -51,6 +52,9 @@ export class ContextRootError extends Error { } export class UserContext { + withObjectStorage(provider: ObjectStorageProvider): this { return this.insertResource('objectStorageProvider', provider); } + withObjectStorageObserver(observer: ObjectStorageObserver): this { return this.insertResource('objectStorageObserver', observer); } + objectStorage(comment: string): ObjectStorage { return new ObjectStorage(this, comment); } /** Runtime-owned outer boundary used by generated Q execution, never SDK code. */ async withQueryRequest(query: SelectQuery, work: () => Promise): Promise { const service = this.requireResource<{ diff --git a/source/teaql-ts/src/core/object-storage.ts b/source/teaql-ts/src/core/object-storage.ts new file mode 100644 index 0000000000000000000000000000000000000000..1533534d0890eeb39b9ef22e4b8d09c39df763a6 --- /dev/null +++ b/source/teaql-ts/src/core/object-storage.ts @@ -0,0 +1,132 @@ +import type { UserContext } from './context'; +import { RuntimeTelemetry, startRuntimeOperation } from './telemetry'; + +export type StorageErrorCode = 'InvalidInput' | 'NotConfigured' | 'NotFound' | 'AccessDenied' | 'Conflict' | 'Timeout' | 'Unavailable' | 'Unsupported' | 'TooLarge' | 'Io'; +export class StorageError extends Error { + constructor(public readonly code: StorageErrorCode) { super(`Object storage: ${code}`); this.name = 'StorageError'; } +} +const inspect = Symbol.for('nodejs.util.inspect.custom'); +export const MAX_OBJECT_PUT_SIZE = 5 * 1024 ** 3; +const invalid = (): never => { throw new StorageError('InvalidInput'); }; +function header(value: string): string { if (typeof value !== 'string' || !value || /[^\x20-\x7e]/.test(value)) invalid(); return value; } +export class ObjectKey { + #value: string; + constructor(value: string) { + if (typeof value !== 'string' || !value || new TextEncoder().encode(value).length > 1024 || /[\x00-\x1f\x7f-\x9f]/.test(value)) invalid(); + // Reject isolated UTF-16 surrogates; TextEncoder would otherwise replace them. + for (let i=0;i=0xd800&&c<=0xdbff){const n=value.charCodeAt(++i);if(!(n>=0xdc00&&n<=0xdfff))invalid();} else if(c>=0xdc00&&c<=0xdfff)invalid(); } + this.#value = value; + } + get value(): string { return this.#value; } + toString(): string { return 'ObjectKey[masked]'; } + [inspect](): string { return this.toString(); } +} +export type ObjectWriteMode = 'create_only' | 'replace'; +export class ObjectPutOptions { + #size: number; #mode: ObjectWriteMode; #contentType: string; #metadata: Readonly>; + constructor(size: number, mode: ObjectWriteMode, contentType='application/octet-stream', metadata: Record={}) { + if (!Number.isSafeInteger(size) || size<0 || !['create_only','replace'].includes(mode)) invalid(); + if(size>MAX_OBJECT_PUT_SIZE)throw new StorageError('TooLarge'); + header(contentType); + for(const [key,value] of Object.entries(metadata)){if(!/^[a-z0-9][a-z0-9-]*$/.test(key))invalid();header(value);} + this.#size=size;this.#mode=mode;this.#contentType=contentType;this.#metadata=Object.freeze({...metadata}); + } + get size(): number{return this.#size;} + get mode(): ObjectWriteMode{return this.#mode;} + get contentType(): string{return this.#contentType;} + get metadata(): Readonly>{return this.#metadata;} + toString(): string{return `ObjectPutOptions[size=${this.size}, mode=${this.mode}, metadata=masked]`;} + [inspect](): string{return this.toString();} +} +export class ObjectMetadata { + #key: ObjectKey;#size:number;#contentType?:string;#etag?:string;#lastModified?:Date;#metadata:Readonly>; + constructor(key:ObjectKey,size:number,contentType?:string,etag?:string,lastModified?:Date,metadata:Record={}){ + if(!Number.isSafeInteger(size)||size<0)throw new StorageError('Unavailable'); + this.#key=key;this.#size=size;this.#contentType=contentType;this.#etag=etag;this.#lastModified=lastModified&&new Date(lastModified);this.#metadata=Object.freeze({...metadata}); + } + get key():ObjectKey{return this.#key;}get size():number{return this.#size;}get contentType():string|undefined{return this.#contentType;} + get etag():string|undefined{return this.#etag;}get lastModified():Date|undefined{return this.#lastModified&&new Date(this.#lastModified);} + get customMetadata():Readonly>{return this.#metadata;} + toString():string{return `ObjectMetadata[size=${this.size}, details=masked]`;}[inspect]():string{return this.toString();} +} +export class ObjectReadUrlOptions { + #seconds:number;#disposition?:string; + constructor(seconds=300,disposition?:'inline'|'attachment',filename?:string){ + if(!Number.isInteger(seconds)||seconds<=0||seconds>604800||disposition!==undefined&&!['inline','attachment'].includes(disposition))invalid(); + if(filename!==undefined){header(filename);if(!disposition||/[\/\\"]/.test(filename))invalid();} + this.#seconds=seconds;this.#disposition=disposition+(filename===undefined?'':`; filename="${filename}"`); + if(disposition===undefined)this.#disposition=undefined; + } + get seconds():number{return this.#seconds;}get contentDisposition():string|undefined{return this.#disposition;} + toString():string{return 'ObjectReadUrlOptions[details=masked]';}[inspect]():string{return this.toString();} +} +export class ObjectReadUrl { + #url:string;#expiresAt:Date; + constructor(url:string,expiresAt:Date){this.#url=url;this.#expiresAt=new Date(expiresAt);} + get url():string{return this.#url;}get expiresAt():Date{return new Date(this.#expiresAt);} + toString():string{return 'ObjectReadUrl[url=masked]';}[inspect]():string{return this.toString();} +} +export type ObjectByteStream = AsyncIterable; +export type StorageInvocation = Readonly<{context:UserContext;comment:string}>; +export interface ObjectStorageProvider { + put(invocation:StorageInvocation,key:ObjectKey,content:ObjectByteStream,options:ObjectPutOptions):Promise; + get(invocation:StorageInvocation,key:ObjectKey):Promise; + head(invocation:StorageInvocation,key:ObjectKey):Promise; + delete(invocation:StorageInvocation,key:ObjectKey):Promise; + getReadUrl(invocation:StorageInvocation,key:ObjectKey,options:ObjectReadUrlOptions):Promise; +} +export type ObjectStorageEvent = Readonly<{operation:string;comment:string;traceId:string;outcome:string;elapsedMs:number}>; +export type ObjectStorageObserver = (event:ObjectStorageEvent)=>void; +function sanitized(error:unknown):StorageError{return error instanceof StorageError?error:new StorageError('Unavailable');} +const safe=(text:unknown):string=>String(text??'').replace(/[\x00-\x1f\x7f-\x9f]/g,' '); +class Receipt { + #ended=false;#started=performance.now();readonly scope; + constructor(private invocation:StorageInvocation,private operation:string){this.scope=startRuntimeOperation(invocation.context.getResource('runtimeTelemetry'),{family:'object_storage',name:operation});} + finish(outcome:string):void{ + if(this.#ended)return;this.#ended=true; + if(outcome==='success')this.scope.success();else this.scope.failure(new StorageError(outcome==='cancelled'?'Io':outcome as StorageErrorCode)); + const event=Object.freeze({operation:this.operation,comment:safe(this.invocation.comment),traceId:safe(this.invocation.context.getResource('traceId')),outcome,elapsedMs:performance.now()-this.#started}); + try{const observer=this.invocation.context.getResource('objectStorageObserver');if(observer)observer(event);else console.info(`ObjectStorage operation=${event.operation} comment=${event.comment} trace=${event.traceId} outcome=${event.outcome} elapsedMs=${event.elapsedMs.toFixed(3)}`);}catch{/* fail open */} + } +} +export class ObjectContent { + #metadata:ObjectMetadata;#stream:ObjectByteStream;#close:()=>Promise; + constructor(metadata:ObjectMetadata,stream:ObjectByteStream,close:()=>Promise){this.#metadata=metadata;this.#stream=stream;this.#close=close;} + get metadata():ObjectMetadata{return this.#metadata;}get stream():ObjectByteStream{return this.#stream;} + close():Promise{return this.#close();} + toString():string{return 'ObjectContent[masked]';}[inspect]():string{return this.toString();} +} +export class ObjectStorage { + #invocation:StorageInvocation;#provider:ObjectStorageProvider; + constructor(context:UserContext,comment:string){ + if(typeof comment!=='string'||!comment.trim())invalid();const provider=context.getResource('objectStorageProvider');if(!provider)throw new StorageError('NotConfigured'); + this.#invocation=Object.freeze({context,comment});this.#provider=provider; + } + async #call(operation:string,work:()=>Promise):Promise{const receipt=new Receipt(this.#invocation,operation);try{const result=await (receipt.scope.run?receipt.scope.run(work):work());receipt.finish('success');return result;}catch(error){const safeError=sanitized(error);receipt.finish(safeError.code);throw safeError;}} + async put(key:ObjectKey,content:ObjectByteStream,options:ObjectPutOptions):Promise{ + const iterator=content[Symbol.asyncIterator]();let done=false; + const owned:AsyncIterableIterator={next:async()=>{const result=await iterator.next();if(result.done)done=true;return result;},return:async()=>{if(!done){done=true;await iterator.return?.();}return {done:true,value:undefined};},[Symbol.asyncIterator](){return this;}}; + return this.#call('put',async()=>{try{return await this.#provider.put(this.#invocation,key,owned,options);}finally{await owned.return!();}}); + } + async putBytes(key:ObjectKey,bytes:Uint8Array,options:ObjectPutOptions):Promise{return this.put(key,(async function*(){yield bytes;})(),options);} + async get(key:ObjectKey):Promise{ + const receipt=new Receipt(this.#invocation,'get');let response:ObjectContent; + try{response=await (receipt.scope.run?receipt.scope.run(()=>this.#provider.get(this.#invocation,key)):this.#provider.get(this.#invocation,key));}catch(error){const safeError=sanitized(error);receipt.finish(safeError.code);throw safeError;} + let closed=false,started=false; + const close=async():Promise=>{if(closed)return;closed=true;try{await response.close();receipt.finish('cancelled');}catch{receipt.finish('Io');throw new StorageError('Io');}}; + const stream: ObjectByteStream = {[Symbol.asyncIterator]:async function*(){ + if(started||closed)throw new StorageError('Io');started=true;let remaining=response.metadata.size; + try{for await(const bytes of response.stream){if(!(bytes instanceof Uint8Array)||bytes.byteLength>remaining)throw new StorageError('Io');remaining-=bytes.byteLength;yield bytes;}if(remaining!==0)throw new StorageError('Io');receipt.finish('success');} + catch{receipt.finish('Io');throw new StorageError('Io');}finally{await close();} + }}; + return new ObjectContent(response.metadata,stream,close); + } + async getBytes(key:ObjectKey,maximumBytes:number):Promise{ + if(!Number.isSafeInteger(maximumBytes)||maximumBytes<0)invalid();const response=await this.get(key); + try{if(response.metadata.size>maximumBytes)throw new StorageError('TooLarge');const chunks:Uint8Array[]=[];let size=0;for await(const block of response.stream){if(block.byteLength>maximumBytes-size)throw new StorageError('TooLarge');size+=block.byteLength;chunks.push(block);}const result=new Uint8Array(size);let offset=0;for(const block of chunks){result.set(block,offset);offset+=block.byteLength;}return result;}finally{await response.close();} + } + head(key:ObjectKey):Promise{return this.#call('head',()=>this.#provider.head(this.#invocation,key));} + async exists(key:ObjectKey):Promise{return (await this.head(key))!==undefined;} + delete(key:ObjectKey):Promise{return this.#call('delete',()=>this.#provider.delete(this.#invocation,key));} + getReadUrl(key:ObjectKey,options=new ObjectReadUrlOptions()):Promise{return this.#call('read_url',()=>this.#provider.getReadUrl(this.#invocation,key,options));} +} diff --git a/source/teaql-ts/src/core/telemetry.ts b/source/teaql-ts/src/core/telemetry.ts index a9587158ff0cc49dffe0496e550fa87e4906529a..57ebd504906323982e73e5ca0c2df0150fb2ec62 100644 --- a/source/teaql-ts/src/core/telemetry.ts +++ b/source/teaql-ts/src/core/telemetry.ts @@ -5,7 +5,8 @@ export type RuntimeOperationFamily = | 'provider' | 'cache' | 'tfp' - | 'audit'; + | 'audit' + | 'object_storage'; export type RuntimeAttributeValue = string | number | boolean; diff --git a/source/teaql-ts/src/index.ts b/source/teaql-ts/src/index.ts index a583d97f3307a2a3b61c3450a445cd7eab250a95..fafdac356574b3412fbc20180d4f8508f4df66b2 100644 --- a/source/teaql-ts/src/index.ts +++ b/source/teaql-ts/src/index.ts @@ -1,5 +1,6 @@ export * from './core/value'; export * from './core/context'; +export * from './core/object-storage'; export * from './core/request-intent'; export * from './core/trace-chain'; export * from './core/mutation-policy'; diff --git a/source/teaql-ts/src/object-storage/s3.ts b/source/teaql-ts/src/object-storage/s3.ts new file mode 100644 index 0000000000000000000000000000000000000000..de063c9103797a15d151ec9d6dcd5e4b111c84ec --- /dev/null +++ b/source/teaql-ts/src/object-storage/s3.ts @@ -0,0 +1,67 @@ +/** Server-only adapter. SDK types and filesystem dependencies stay outside core. */ +import {S3Client,PutObjectCommand,GetObjectCommand,HeadObjectCommand,HeadBucketCommand,DeleteObjectCommand} from '@aws-sdk/client-s3'; +import {getSignedUrl} from '@aws-sdk/s3-request-presigner'; +import {mkdtemp,open,rm} from 'node:fs/promises'; +import {createReadStream} from 'node:fs'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {Readable} from 'node:stream'; +import {ObjectStorageProvider,StorageInvocation,ObjectKey,ObjectByteStream,ObjectPutOptions,ObjectMetadata,ObjectContent,ObjectReadUrlOptions,ObjectReadUrl,StorageError} from '../core/object-storage'; + +function translate(error:unknown):StorageError{ + if(error instanceof StorageError)return error; + const e=error as {name?:string;$metadata?:{httpStatusCode?:number}}; + if(e.name==='NoSuchBucket')return new StorageError('Unavailable'); + if(e.name==='TimeoutError'||e.name==='RequestTimeout')return new StorageError('Timeout'); + switch(e.$metadata?.httpStatusCode){case 401:case 403:return new StorageError('AccessDenied');case 404:return new StorageError('NotFound');case 409:case 412:return new StorageError('Conflict');case 408:case 504:return new StorageError('Timeout');case 413:return new StorageError('TooLarge');case 501:return new StorageError('Unsupported');case 400:case 422:return new StorageError('InvalidInput');default:return new StorageError('Unavailable');} +} +function metadata(key:ObjectKey,result:{ContentLength?:number;ContentType?:string;ETag?:string;LastModified?:Date;Metadata?:Record}):ObjectMetadata{ + if(!Number.isSafeInteger(result.ContentLength)||!result.ETag?.trim())throw new StorageError('Unavailable'); + return new ObjectMetadata(key,result.ContentLength!,result.ContentType,result.ETag,result.LastModified,result.Metadata); +} +export class S3ObjectStorage implements ObjectStorageProvider { + #client:S3Client;#bucket:string; + constructor(client:S3Client,bucket:string){if(!bucket.trim())throw new StorageError('InvalidInput');this.#client=client;this.#bucket=bucket;} + async put(_:StorageInvocation,key:ObjectKey,content:ObjectByteStream,options:ObjectPutOptions):Promise{ + let directory:string; + try{directory=await mkdtemp(join(tmpdir(),'teaql-object-'));}catch{throw new StorageError('Io');} + try{ + const file=join(directory,'upload');const handle=await open(file,'wx',0o600);let size=0; + try{for await(const chunk of content){if(!(chunk instanceof Uint8Array))throw new StorageError('InvalidInput');size+=chunk.byteLength;if(size>options.size)throw new StorageError('InvalidInput');let offset=0;while(offsetsetTimeout(resolve,50*(attempt+1)));} + finally{body.destroy();} + } + throw new StorageError('Unavailable'); + }catch(error){throw error instanceof StorageError?error:new StorageError('Io');} + finally{try{await rm(directory,{recursive:true,force:true});}catch{throw new StorageError('Io');}} + } + async get(_:StorageInvocation,key:ObjectKey):Promise{ + let result; + try{result=await this.#client.send(new GetObjectCommand({Bucket:this.#bucket,Key:key.value}));}catch(error){throw translate(error);} + const body=result.Body as Readable|undefined; + try{if(!body||!body[Symbol.asyncIterator])throw new StorageError('Unavailable');return new ObjectContent(metadata(key,result),body as unknown as ObjectByteStream,async()=>{body.destroy();});} + catch(error){body?.destroy();throw translate(error);} + } + async head(_:StorageInvocation,key:ObjectKey):Promise{ + try{return metadata(key,await this.#client.send(new HeadObjectCommand({Bucket:this.#bucket,Key:key.value})));} + catch(error){const safeError=translate(error);if(safeError.code!=='NotFound')throw safeError;} + try{await this.#client.send(new HeadBucketCommand({Bucket:this.#bucket}));return undefined;} + catch(error){const safeError=translate(error);throw safeError.code==='NotFound'?new StorageError('Unavailable'):safeError;} + } + async delete(_:StorageInvocation,key:ObjectKey):Promise{try{await this.#client.send(new DeleteObjectCommand({Bucket:this.#bucket,Key:key.value}));}catch(error){throw translate(error);}} + async getReadUrl(_:StorageInvocation,key:ObjectKey,options:ObjectReadUrlOptions):Promise{ + const date=new Date(); + try{const url=await getSignedUrl(this.#client,new GetObjectCommand({Bucket:this.#bucket,Key:key.value,ResponseContentDisposition:options.contentDisposition}),{expiresIn:options.seconds,signingDate:date}); + const headers=new URL(url).searchParams.get('X-Amz-SignedHeaders');if(headers&&headers.toLowerCase()!=='host')throw new StorageError('Unsupported'); + return new ObjectReadUrl(url,new Date(date.getTime()+options.seconds*1000)); + }catch(error){throw translate(error);} + } + close():void{this.#client.destroy();} + toString():string{return 'S3ObjectStorage[configuration=masked]';} + [Symbol.for('nodejs.util.inspect.custom')]():string{return this.toString();} +} diff --git a/source/teaql-ts/test.ts b/source/teaql-ts/test.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce4554b71366b2eb7971c95127e0cab001518e8b --- /dev/null +++ b/source/teaql-ts/test.ts @@ -0,0 +1,34 @@ +import fetch from 'node-fetch'; +import { TeaQLClient } from './src/tfp/client'; +import { Q } from './src/generated/Q'; +import { QueryParser } from './src/parser/dsl'; + +const client = new TeaQLClient({ + baseUrl: 'http://127.0.0.1:3001', + fetch: fetch as any +}); + +const context = { client }; + +async function runTest() { + try { + console.log("Testing QueryParser (Filter)..."); + const queryStr1 = 'Q.tasks().withNameContaining("bug")'; + console.log("Parsing:", queryStr1); + const parsedRequest1 = QueryParser.parse(queryStr1); + const parsedTasks1 = await parsedRequest1.executeForList(context); + console.log("Parsed Tasks result:", parsedTasks1); + + console.log("\nTesting QueryParser (Aggregation + GroupBy)..."); + const queryStr2 = 'Q.taskStatuses().groupByDisplayOrder().sumProgress()'; + console.log("Parsing:", queryStr2); + const parsedRequest2 = QueryParser.parse(queryStr2); + const parsedTasks2 = await parsedRequest2.executeForList(context); + console.log("Parsed Aggregation result:", parsedTasks2); + + } catch (err) { + console.error(err); + } +} + +runTest(); diff --git a/source/teaql-ts/tests/object-storage.test.ts b/source/teaql-ts/tests/object-storage.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..73a1f2b72e83ed8a4c53d9294069040e782eb35f --- /dev/null +++ b/source/teaql-ts/tests/object-storage.test.ts @@ -0,0 +1,56 @@ +import {inspect} from 'node:util'; +import {createServer,Server} from 'node:http'; +import {AddressInfo} from 'node:net'; +import {S3Client} from '@aws-sdk/client-s3'; +import {UserContext} from '../src/core/context'; +import {ObjectKey,ObjectPutOptions,ObjectReadUrlOptions,ObjectReadUrl,ObjectMetadata,ObjectContent,ObjectStorageProvider,StorageInvocation,StorageError,ObjectStorageEvent,MAX_OBJECT_PUT_SIZE} from '../src/core/object-storage'; +import {S3ObjectStorage} from '../src/object-storage/s3'; + +class Stub implements ObjectStorageProvider { + size=3;error?:StorageError;seen?:StorageInvocation;closes=0; + async put(i:StorageInvocation,key:ObjectKey,_:AsyncIterable,o:ObjectPutOptions):Promise{this.seen=i;return new ObjectMetadata(key,o.size);} + async head(i:StorageInvocation):Promise{this.seen=i;if(this.error)throw this.error;return undefined;} + async get(_:StorageInvocation,key:ObjectKey):Promise{return new ObjectContent(new ObjectMetadata(key,this.size),(async function*(){yield new Uint8Array([1,2,3]);})(),async()=>{this.closes++;});} + async delete():Promise{} + async getReadUrl():Promise{throw new StorageError('Unsupported');} +} +const key=new ObjectKey('PRIVATE-KEY-CANARY'); +test('object storage validates options and masks private representations',()=>{ + for(const value of ['', 'x'.repeat(1025),'bad\nkey','\ud800'])expect(()=>new ObjectKey(value)).toThrow(StorageError); + expect(()=>new ObjectPutOptions(MAX_OBJECT_PUT_SIZE+1,'create_only')).toThrow('TooLarge'); + expect(()=>new ObjectPutOptions(0,'create_only','bad\r\nheader')).toThrow('InvalidInput'); + expect(()=>new ObjectPutOptions(0,'create_only',undefined,{'Bad Key':'value'})).toThrow('InvalidInput'); + for(const lifetime of [0,-1,0.5,604801])expect(()=>new ObjectReadUrlOptions(lifetime)).toThrow('InvalidInput'); + expect(()=>new ObjectReadUrlOptions(1,'attachment','bad\nname')).toThrow('InvalidInput'); + const metadata={tag:'PRIVATE-METADATA-CANARY'},options=new ObjectPutOptions(0,'create_only',undefined,metadata);metadata.tag='changed';expect(options.metadata.tag).toBe('PRIVATE-METADATA-CANARY');expect(Object.isFrozen(options.metadata)).toBe(true); + for(const value of [key,options,new ObjectReadUrl('PRIVATE-URL-CANARY',new Date()),new ObjectMetadata(key,0,'PRIVATE-TYPE-CANARY')])expect(inspect(value)).not.toContain('CANARY'); +}); +test('object storage retains context and propagates denied/unavailable errors',async()=>{ + expect(()=>new UserContext().objectStorage('read')).toThrow('NotConfigured');const provider=new Stub(),context=new UserContext().withObjectStorage(provider); + expect(()=>context.objectStorage(' ')).toThrow('InvalidInput');const storage=context.objectStorage('read attachment');expect(await storage.exists(key)).toBe(false);expect(provider.seen?.context).toBe(context);expect(provider.seen?.comment).toBe('read attachment'); + for(const code of ['AccessDenied','Unavailable'] as const){provider.error=new StorageError(code);await expect(storage.exists(key)).rejects.toMatchObject({code});} +}); +test('object storage stream ownership, limits, completion and observer safety',async()=>{ + const provider=new Stub(),events:ObjectStorageEvent[]=[],context=new UserContext().withObjectStorage(provider).withObjectStorageObserver(event=>events.push(event));const storage=context.objectStorage('read attachment'); + let closes=0;const input={ [Symbol.asyncIterator](){return {next:async()=>({done:false as const,value:new Uint8Array()}),return:async()=>{closes++;return {done:true as const,value:undefined};}};}}; + await storage.put(key,input,new ObjectPutOptions(0,'create_only'));expect(closes).toBe(1); + const content=await storage.get(key);for await(const _ of content.stream)break;expect(events[events.length-1]?.outcome).toBe('cancelled');expect(provider.closes).toBe(1); + expect(await storage.getBytes(key,3)).toEqual(new Uint8Array([1,2,3]));expect(events[events.length-1]?.outcome).toBe('success'); + await expect(storage.getBytes(key,2)).rejects.toMatchObject({code:'TooLarge'}); + for(const size of [2,4]){provider.size=size;await expect(storage.getBytes(key,5)).rejects.toMatchObject({code:'Io'});} + expect(inspect(events)).not.toContain('CANARY');context.withObjectStorageObserver(()=>{throw new Error('observer failure');});expect(await storage.exists(key)).toBe(false); +}); +test('S3 actual HTTP errors, malformed success and invalid streams do not become absence',async()=>{ + let status=403,calls=0;const server=createServer((request,response)=>{calls++;response.statusCode=request.url?.replace(/\/$/,'')==='/bucket'&&status===404?200:status;response.end();}); + await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve)); + const endpoint=`http://127.0.0.1:${(server.address() as AddressInfo).port}`; + const provider=new S3ObjectStorage(new S3Client({endpoint,region:'us-east-1',forcePathStyle:true,maxAttempts:1,credentials:{accessKeyId:'test',secretAccessKey:'secret'}}),'bucket'); + const storage=new UserContext().withObjectStorage(provider).withObjectStorageObserver(()=>{}).objectStorage('verify HTTP failures'); + try{ + for(const [code,expected] of [[403,'AccessDenied'],[500,'Unavailable'],[503,'Unavailable'],[408,'Timeout'],[504,'Timeout'],[501,'Unsupported']] as const){status=code;const error=await storage.exists(key).catch(error=>error);expect(error).toBeInstanceOf(StorageError);expect(error.code).toBe(expected);expect(error.cause).toBeUndefined();expect(error.message).not.toContain('CANARY');} + status=404;expect(await storage.exists(key)).toBe(false);status=200;await expect(storage.exists(key)).rejects.toMatchObject({code:'Unavailable'}); + const before=calls;for(const [content,size] of [[new Uint8Array([1,2]),1],[new Uint8Array([1]),2]] as const)await expect(storage.putBytes(key,content,new ObjectPutOptions(size,'create_only'))).rejects.toMatchObject({code:'InvalidInput'}); + await expect(storage.put(key,(async function*(){throw new Error('PRIVATE-INPUT-CANARY');yield new Uint8Array();})(),new ObjectPutOptions(1,'create_only'))).rejects.toMatchObject({code:'Io'}); + expect(calls).toBe(before);await new Promise(resolve=>server.close(()=>resolve()));await expect(storage.exists(key)).rejects.toMatchObject({code:'Unavailable'}); + }finally{server.close();provider.close();} +}); diff --git a/work-items.html b/work-items.html index 5678d61125ebad3bb4c143319b0d5ca5b52015e5..b23ddbf029e4c4748ef1196d6322f43bc53c3919 100644 --- a/work-items.html +++ b/work-items.html @@ -5,9 +5,9 @@ TeaQL Browser SQLite WorkItems - - - + + +