diff --git a/.changeset/wicked-lions-wonder.md b/.changeset/wicked-lions-wonder.md deleted file mode 100644 index cc5fc64..0000000 --- a/.changeset/wicked-lions-wonder.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@_linked/core": minor ---- - -- Added `Shape.selectAll()` plus nested `selectAll()` support on sub-queries. -- Added inherited property deduplication via `NodeShape.getUniquePropertyShapes()` so subclass overrides win by label and are selected once. -- Improved `selectAll()` type inference (including nested queries) and excluded base `Shape` keys from inferred results. -- Added registration-time override guards: `minCount` cannot be lowered, `maxCount` cannot be increased, and `nodeKind` cannot be widened. -- Fixed `createPropertyShape` to preserve explicit `minCount: 0` / `maxCount: 0`. -- Expanded tests and README documentation for `selectAll`, CRUD return types, and multi-value update semantics. diff --git a/AGENTS.md b/AGENTS.md index 571a5ad..45fe0af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,8 @@ ls docs/ # list all docs head -4 docs/*.md # get summaries (or replace * with a specific file) ``` +Each package also has a `README.md` with API docs and a `## Changelog` at the bottom. + ## Planning and implementation workflow ### When to plan @@ -31,7 +33,7 @@ Any task that changes package code requires a plan. Simple checks, info gatherin ### Creating a plan 1. **Inspect the relevant code thoroughly** before writing anything. Read the source files, tests, and existing docs that relate to the task. -2. Create a new doc in `docs/` with the next 3-digit prefix (e.g. `005-add-filter-support.md`) **for new feature/PR scope**. For follow-ups in the same thread/PR scope, update the existing plan doc. Start with YAML frontmatter. +2. Create a new doc in `docs/` with the next 3-digit prefix (e.g. `005-add-filter-support.md`). Start with YAML frontmatter. 3. Write the plan with these sections: - **Key considerations and choices** — tradeoffs, open questions, alternatives - **Potential problems** — what could go wrong, edge cases @@ -54,4 +56,4 @@ Any task that changes package code requires a plan. Simple checks, info gatherin Before committing final changes or preparing a PR: 1. **Consolidate the plan doc** — collapse alternatives into the choices that were made, summarize implementation details and breaking changes, keep a brief problems section if relevant, remove anything redundant for future readers. -2. **Run changesets** Ask the user if this is a patch/minor/major change, then run `npx changesets/cli` and provide an appropriate changelog message covering behavior changes, new APIs, breaking changes, and migration steps. +2. **Update `## Changelog`** in each affected package's `README.md` — user-facing entry covering behavior changes, new APIs, breaking changes, and migration steps. diff --git a/README.md b/README.md index 851313f..c98a94c 100644 --- a/README.md +++ b/README.md @@ -85,35 +85,41 @@ export class Person extends Shape { ## Queries: Create, Select, Update, Delete Queries are expressed with the same Shape classes and compile to a query object that a store executes. -Use this section as a quick start. Detailed query variations are documented in `Query examples` below. -A few quick examples: - -**1) Select one field for all matching nodes** ```typescript +/* Result: Array<{id: string; name: string}> */ const names = await Person.select((p) => p.name); -/* names: {id: string; name: string}[] */ -``` -**2) Select all decorated fields of nested related nodes** -```typescript -const allFriends = await Person.select((p) => p.knows.selectAll()); -/* allFriends: { - id?: string; - knows: { - id?: string; - ...all decorated Person fields... - }[] - }[] */ -``` - -**3) Apply a simple mutation** -```typescript const myNode = {id: 'https://my.app/node1'}; +/* Result: {id: string; name: string} | null */ +const person = await Person.select(myNode, (p) => p.name); +const missing = await Person.select({id: 'https://my.app/missing'}, (p) => p.name); // null + +/* Result: {id: string} & UpdatePartial */ +const created = await Person.create({ + name: 'Alice', + knows: [{id: 'https://my.app/node2'}], +}); + const updated = await Person.update(myNode, { name: 'Alicia', }); -/* updated: {id: string} & UpdatePartial */ + +// Overwrite a multi-value property +const overwriteFriends = await Person.update(myNode, { + knows: [{id: 'https://my.app/node2'}], +}); + +// Add/remove items in a multi-value property +const addRemoveFriends = await Person.update(myNode, { + knows: { + add: [{id: 'https://my.app/node3'}], + remove: [{id: 'https://my.app/node2'}], + }, +}); + +/* Result: {deleted: Array<{id: string}>, count: number} */ +await Person.delete(myNode); ``` ## Storage configuration @@ -166,13 +172,10 @@ Result types are inferred from your Shape definitions and the selected paths. Ex #### Basic selection ```typescript -/* names: {id: string; name: string}[] */ +/* Result: Array<{id: string; name: string}> */ const names = await Person.select((p) => p.name); -/* friends: { - id: string; - knows: { id: string }[] -}[] */ +/* Result: Array<{id: string; knows: Array<{id: string}>}> */ const friends = await Person.select((p) => p.knows); const dates = await Person.select((p) => [p.birthDate, p.name]); @@ -199,12 +202,6 @@ const deep = await Person.select((p) => p.knows.bestFriend.name); const detailed = await Person.select((p) => p.knows.select((f) => f.name), ); - -const allPeople = await Person.selectAll(); - -const detailedAll = await Person.select((p) => - p.knows.selectAll(), -); ``` #### Where + equals @@ -264,9 +261,7 @@ const custom = await Person.select((p) => ({ })); ``` -#### Query As (type casting to a sub shape) -If person.pets returns an array of Pets. And Dog extends Pet. -And you want to select properties of those pets that are dogs: +#### Query As (type casting) ```typescript const guards = await Person.select((p) => p.pets.as(Dog).guardDogLevel); ``` @@ -298,131 +293,29 @@ const preloaded = await Person.select((p) => [ ]); ``` -#### Create - +#### Create / Update / Delete ```typescript /* Result: {id: string} & UpdatePartial */ const created = await Person.create({name: 'Alice'}); -``` -Where UpdatePartial reflects the created properties. - -#### Update -Update will patch any property that you send as payload and leave the rest untouched. -```typescript -/* Result: {id: string} & UpdatePartial */ const updated = await Person.update({id: 'https://my.app/node1'}, {name: 'Alicia'}); -``` -Returns: -```json -{ - id:"https://my.app/node1", - name:"Alicia" -} -``` -**Updating multi-value properties** -When updating a property that holds multiple values (one that returns an array in the results), you can either overwrite all the values with a new explicit array of values, or delete from/add to the current values. - -To overwrite all values: -```typescript -// Overwrite the full set of "knows" values. -const overwriteFriends = await Person.update({id: 'https://my.app/person1'}, { - knows: [{id: 'https://my.app/person2'}], +// Overwrite a multi-value property +const overwriteFriends = await Person.update({id: 'https://my.app/node1'}, { + knows: [{id: 'https://my.app/node2'}], }); -``` -The result will contain an object with `updatedTo`, to indicate that previous values were overwritten to this new set of values: -```json -{ - id: "https://my.app/person1", - knows: { - updatedTo: [{id:"https://my.app/person2"}], - } -} -``` -To make incremental changes to the current set of values you can provide an object with `add` and/or `remove` keys: -```typescript -// Add one value and remove one value without replacing the whole set. -const addRemoveFriends = await Person.update({id: 'https://my.app/person1'}, { +// Add/remove items in a multi-value property +const addRemoveFriends = await Person.update({id: 'https://my.app/node1'}, { knows: { - add: [{id: 'https://my.app/person2'}], - remove: [{id: 'https://my.app/person3'}], + add: [{id: 'https://my.app/node3'}], + remove: [{id: 'https://my.app/node2'}], }, }); -``` -This returns an object with the added and removed items -```json -{ - id: "https://my.app/person1", - knows: { - added?: [{id:"https://my.app/person2"}, - removed?: [{id:"https://my.app/person3"}], - } -} -``` - - -#### Delete -To delete a node entirely: - -```typescript -/* Result: {deleted: Array<{id: string}>, count: number} */ -const deleted = await Person.delete({id: 'https://my.app/node1'}); -``` -Returns -```json -{ - deleted:[ - {id:"https://my.app/node1"} - ], - count:1 -} -``` - -To delete multiple nodes pass an array: - -```typescript -/* Result: {deleted: Array<{id: string}>, count: number} */ -const deleted = await Person.delete([{id: 'https://my.app/node1'},{id: 'https://my.app/node2'}]); -``` - - -## Extending shapes - -Shape classes can extend other shape classes. Subclasses inherit property shapes from their superclasses and may override them. -This example assumes `Person` from the `Shapes` section above. - -```typescript -import {literalProperty} from '@_linked/core/shapes/SHACL'; -import {createNameSpace} from '@_linked/core/utils/NameSpace'; -import {linkedShape} from './package'; - -const schema = createNameSpace('https://schema.org/'); -const EmployeeClass = schema('Employee'); -const name = schema('name'); -const employeeId = schema('employeeId'); -@linkedShape -export class Employee extends Person { - static targetClass = EmployeeClass; - - // Override inherited "name" with stricter constraints (still maxCount: 1) - @literalProperty({path: name, required: true, minLength: 2, maxCount: 1}) - declare name: string; - - @literalProperty({path: employeeId, required: true, maxCount: 1}) - declare employeeId: string; -} +await Person.delete({id: 'https://my.app/node1'}); ``` -Override behavior: - -- `NodeShape.getUniquePropertyShapes()` returns one property shape per label, with subclass overrides taking precedence. -- Overrides must be tighten-only for `minCount`, `maxCount`, and `nodeKind` (widening is rejected at registration time). -- If an override omits `minCount`, `maxCount`, or `nodeKind`, inherited values are kept. -- Current scope: compatibility checks for `datatype`, `class`, and `pattern` are not enforced yet. - ## TODO - Allow `preloadFor` to accept another query (not just a component). diff --git a/docs/002-select-all-property-shapes.md b/docs/002-select-all-property-shapes.md deleted file mode 100644 index 6f57af5..0000000 --- a/docs/002-select-all-property-shapes.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -summary: Implement and refine selectAll across shape, sub-query, inheritance dedupe, and typing behavior -packages: [core] ---- - -## Key considerations and choices - -- `selectAll` was kept explicit (`Shape.selectAll()` and nested `p.friends.selectAll()` / `p.bestFriend.selectAll()`) and implemented by reusing existing `select(...)` internals. -- Inherited properties are included, with label-based dedupe so subclass overrides win and appear once. -- Unique-property-shape behavior was added to metadata as `NodeShape.getUniquePropertyShapes(): PropertyShape[]` so callers can inspect selected metadata directly. -- `getPropertyShapes(true)` semantics were preserved; dedupe behavior is isolated to `getUniquePropertyShapes()`. -- Nested `selectAll` type inference was improved enough to expose common nested fields in compile tests without changing runtime query behavior. -- `selectAll` key filtering was tightened to exclude base `Shape` instance keys from inferred property unions. -- Override validation was finalized as tighten-only for `minCount`, `maxCount`, and `nodeKind`, with omitted override fields inheriting base constraints silently. -- Property-shape count parsing was fixed to preserve explicit `0` values for `minCount`/`maxCount`. - -## Problems addressed - -- Inheritance-chain regressions were avoided by not changing `getPropertyShapes(true)` and adding a separate dedupe API. -- Override precedence is stable: dedupe order follows subclass-first traversal so overridden labels resolve to subclass property shapes. -- Over-broad override constraints are now blocked at registration time with explicit errors for lowered `minCount`, raised `maxCount`, and widened `nodeKind`. -- Mixed override cases (partial explicit override + inherited fields) are covered and validated in metadata tests. - -## Remaining limits - -- Compatibility checks for override fields like `datatype`, `class`, and `pattern` are not enforced yet. -- `selectAll` key inference is stricter than before but may still include non-decorated subclass members in some cases due to current `keyof`-based inference bounds. -- Shapes with no decorated properties still produce empty select projections (current intended behavior). -- Decorator-level contradictions like `required: true` with `minCount: 0` still follow existing precedence (`required` wins). - -## Phases - -1. **Initial `selectAll` implementation** ✅ - - Add `Shape.selectAll(...)` and reuse `select(...)` internals. - - Validate with query generation tests. -2. **Nested sub-query support** ✅ - - Add `selectAll()` on `QueryShapeSet` and `QueryShape`. - - Validate query object shape for `p.friends.selectAll()` and `p.bestFriend.selectAll()`. -3. **Inherited override dedupe** ✅ - - Ensure overridden labels appear once and subclass override wins. - - Add regression fixture/tests with subclass overrides. -4. **Move unique-property API onto `NodeShape`** ✅ - - Add `NodeShape.getUniquePropertyShapes()` and migrate `selectAll` call sites. - - Keep `getPropertyShapes(true)` semantics unchanged. -5. **Simplify API + improve nested typing** ✅ - - Make `getUniquePropertyShapes()` always use inheritance (remove boolean flag). - - Improve nested `selectAll` typing and add type tests for nested fields. -6. **Docs/changelog consolidation** ✅ - - Update README examples, type-inference notes, and changelog entries. - - Consolidate all selectAll planning history into this document. -7. **Registration-time override guard (tighten-only)** ✅ - - Enforce override checks for minCount/maxCount/nodeKind in property registration. - - Keep omitted override fields inheriting silently from super property shape. - - Add metadata tests for allowed and rejected override cases, including positive tighten cases and mixed inheritance. -8. **Docs cleanup + key-filter tightening follow-up** ✅ - - Trim intro `README` `selectAll` examples to one concise, shape-consistent example. - - Tighten `selectAll` key filtering to exclude base `Shape` instance keys from inferred property unions. -9. **Count parsing regression fix (`0` values)** ✅ - - Treat `minCount: 0` and `maxCount: 0` as explicit values in `createPropertyShape`. - - Add metadata tests for explicit zero-count persistence and override guarding from `0 -> 1`. - -## Implementation summary - -- Added top-level `Shape.selectAll(...)` and nested `selectAll()` support on `QueryShape` and `QueryShapeSet`. -- Added `NodeShape.getUniquePropertyShapes()` for deduped inherited property shape resolution with subclass-first precedence. -- Updated all `selectAll` paths to derive property labels from `getUniquePropertyShapes()`. -- Added runtime query tests for top-level and nested `selectAll`, plus dedupe regression coverage using subclass overrides. -- Added compile-only type tests for top-level and nested `selectAll` field inference. -- Tightened `selectAll` key filtering to exclude base `Shape` instance keys from inferred result unions. -- Added registration-time tighten-only override guards (`minCount`, `maxCount`, `nodeKind`) with explicit metadata test coverage for reject and allow cases. -- Fixed `createPropertyShape` count parsing to preserve explicit `0` values for `minCount`/`maxCount`. -- Updated `README.md` examples/changelog and documented current override-compatibility scope. diff --git a/src/queries/SelectQuery.ts b/src/queries/SelectQuery.ts index da8efd8..c25ab19 100644 --- a/src/queries/SelectQuery.ts +++ b/src/queries/SelectQuery.ts @@ -144,18 +144,6 @@ export type QueryShapeProps< [P in keyof T]: ToQueryBuilderObject, P>; }; -export type SelectAllQueryResponse = Array< - QueryShapeProps[Exclude< - Extract< - { - [K in keyof T]-?: T[K] extends (...args: any[]) => any ? never : K; - }[keyof T], - string - >, - Extract - >] ->; - /** * This type states that the ShapeSet has access to the same methods as the shape of all the items in the set * (this is enabled with the QueryShapeSet.proxifyShapeSet method) @@ -1219,20 +1207,6 @@ export class QueryShapeSet< return subQuery as any; } - selectAll(): SelectQueryFactory< - S, - SelectAllQueryResponse, - QueryShapeSet - > { - let leastSpecificShape = this.getOriginalValue().getLeastSpecificShape(); - const propertyLabels = leastSpecificShape.shape - .getUniquePropertyShapes() - .map((propertyShape) => propertyShape.label); - return this.select((shape) => - propertyLabels.map((label) => (shape as any)[label]), - ); - } - some(validation: WhereClause): SetEvaluation { return this.someOrEvery(validation, WhereMethods.SOME); } @@ -1391,22 +1365,6 @@ export class QueryShape< return subQuery as any; } - selectAll(): SelectQueryFactory< - S, - SelectAllQueryResponse, - QueryShape - > { - let leastSpecificShape = getShapeClass( - (this.getOriginalValue() as Shape).nodeShape.id, - ); - const propertyLabels = leastSpecificShape.shape - .getUniquePropertyShapes() - .map((propertyShape) => propertyShape.label); - return this.select((shape) => - propertyLabels.map((label) => (shape as any)[label]), - ); - } - // count(countable: QueryBuilderObject, resultKey?: string): SetSize { // return new SetSize(this, countable, resultKey); // // return this._count; diff --git a/src/shapes/SHACL.ts b/src/shapes/SHACL.ts index 766b581..d182a48 100644 --- a/src/shapes/SHACL.ts +++ b/src/shapes/SHACL.ts @@ -198,10 +198,6 @@ export interface PropertyShapeConfig { sortBy?: PropertyPathInputList; } -const EXPLICIT_NODE_KIND_SYMBOL = Symbol('explicitNodeKind'); -const EXPLICIT_MIN_COUNT_SYMBOL = Symbol('explicitMinCount'); -const EXPLICIT_MAX_COUNT_SYMBOL = Symbol('explicitMaxCount'); - export interface ParameterConfig { optional?: number; } @@ -259,18 +255,6 @@ export class NodeShape extends Shape { return res; } - getUniquePropertyShapes(): PropertyShape[] { - const uniquePropertyShapes: PropertyShape[] = []; - const seen = new Set(); - this.getPropertyShapes(true).forEach((propertyShape) => { - if (!seen.has(propertyShape.label)) { - seen.add(propertyShape.label); - uniquePropertyShapes.push(propertyShape); - } - }); - return uniquePropertyShapes; - } - getPropertyShape( label: string, checkSubShapes: boolean = true, @@ -439,20 +423,7 @@ export function registerPropertyShape( shape: NodeShape, propertyShape: PropertyShape, ) { - const inherited = shape.getPropertyShape(propertyShape.label, true); const existing = shape.getPropertyShape(propertyShape.label, false); - if (!existing && inherited) { - if (!(propertyShape as any)[EXPLICIT_MIN_COUNT_SYMBOL]) { - propertyShape.minCount = inherited.minCount; - } - if (!(propertyShape as any)[EXPLICIT_MAX_COUNT_SYMBOL]) { - propertyShape.maxCount = inherited.maxCount; - } - if (!(propertyShape as any)[EXPLICIT_NODE_KIND_SYMBOL]) { - propertyShape.nodeKind = inherited.nodeKind; - } - validateOverrideTightening(shape, inherited, propertyShape); - } if (existing) { Object.assign(existing, propertyShape); return existing; @@ -462,85 +433,6 @@ export function registerPropertyShape( return propertyShape; } -const ATOMIC_NODE_KINDS = [ - shacl.BlankNode.id, - shacl.IRI.id, - shacl.Literal.id, -]; - -const nodeKindToAtomics = (nodeKind?: NodeReferenceValue): Set => { - if (!nodeKind?.id) { - return new Set(); - } - switch (nodeKind.id) { - case shacl.BlankNode.id: - case shacl.IRI.id: - case shacl.Literal.id: - return new Set([nodeKind.id]); - case shacl.BlankNodeOrIRI.id: - return new Set([shacl.BlankNode.id, shacl.IRI.id]); - case shacl.IRIOrLiteral.id: - return new Set([shacl.IRI.id, shacl.Literal.id]); - case shacl.BlankNodeOrLiteral.id: - return new Set([shacl.BlankNode.id, shacl.Literal.id]); - default: - return new Set(ATOMIC_NODE_KINDS); - } -}; - -const throwOverrideError = ( - shape: NodeShape, - propertyShape: PropertyShape, - message: string, -) => { - throw new Error( - `Invalid override for ${shape.label}.${propertyShape.label}: ${message}`, - ); -}; - -const validateOverrideTightening = ( - shape: NodeShape, - base: PropertyShape, - override: PropertyShape, -) => { - if ( - typeof base.minCount === 'number' && - typeof override.minCount === 'number' && - override.minCount < base.minCount - ) { - throwOverrideError( - shape, - override, - `minCount cannot be lowered (${base.minCount} -> ${override.minCount}).`, - ); - } - - if ( - typeof base.maxCount === 'number' && - typeof override.maxCount === 'number' && - override.maxCount > base.maxCount - ) { - throwOverrideError( - shape, - override, - `maxCount cannot be increased (${base.maxCount} -> ${override.maxCount}).`, - ); - } - - if (base.nodeKind && override.nodeKind) { - const baseKinds = nodeKindToAtomics(base.nodeKind); - const overrideKinds = nodeKindToAtomics(override.nodeKind); - const widensNodeKind = [...overrideKinds].some((kind) => !baseKinds.has(kind)); - if (widensNodeKind) { - throwOverrideError( - shape, - override, - `nodeKind cannot be widened (${base.nodeKind.id} -> ${override.nodeKind.id}).`, - ); - } - } -}; - export function createPropertyShape< Config extends LiteralPropertyShapeConfig | ObjectPropertyShapeConfig, >( @@ -562,17 +454,13 @@ export function createPropertyShape< if (config.required) { propertyShape.minCount = 1; - } else if (config.minCount !== undefined) { + } else if (config.minCount) { propertyShape.minCount = config.minCount; } - (propertyShape as any)[EXPLICIT_MIN_COUNT_SYMBOL] = - config.required === true || config.minCount !== undefined; - if (config.maxCount !== undefined) { + if (config.maxCount) { propertyShape.maxCount = config.maxCount; } - (propertyShape as any)[EXPLICIT_MAX_COUNT_SYMBOL] = - config.maxCount !== undefined; if ((config as LiteralPropertyShapeConfig).datatype) { propertyShape.datatype = toPlainNodeRef( (config as LiteralPropertyShapeConfig).datatype, @@ -605,8 +493,6 @@ export function createPropertyShape< } propertyShape.nodeKind = normalizeNodeKind(config.nodeKind, defaultNodeKind); - (propertyShape as any)[EXPLICIT_NODE_KIND_SYMBOL] = - config.nodeKind !== undefined; if (shapeClass) { onShapeSetup(shapeClass, (shape: NodeShape) => { diff --git a/src/shapes/Shape.ts b/src/shapes/Shape.ts index ccbfb65..ff7e544 100644 --- a/src/shapes/Shape.ts +++ b/src/shapes/Shape.ts @@ -14,7 +14,6 @@ import { QueryBuildFn, QueryResponseToResultType, QueryShape, - SelectAllQueryResponse, SelectQueryFactory, } from '../queries/SelectQuery.js'; import type {IQueryParser} from '../interfaces/IQueryParser.js'; @@ -206,47 +205,6 @@ export abstract class Shape { return query.patchResultPromise(p); } - /** - * Select all decorated properties of this shape. - * Returns a single result if a single subject is provided, or an array of results if no subject is provided. - */ - static selectAll< - ShapeType extends Shape, - ResultType = QueryResponseToResultType< - SelectAllQueryResponse, - ShapeType - >[], - >( - this: {new (...args: any[]): ShapeType; queryParser: IQueryParser}, - ): Promise & PatchedQueryPromise; - static selectAll< - ShapeType extends Shape, - ResultType = QueryResponseToResultType< - SelectAllQueryResponse, - ShapeType - >, - >( - this: {new (...args: any[]): ShapeType; queryParser: IQueryParser}, - subject: ShapeType | QResult, - ): Promise & PatchedQueryPromise; - static selectAll< - ShapeType extends Shape, - ResultType = QueryResponseToResultType< - SelectAllQueryResponse, - ShapeType - >[], - >( - this: {new (...args: any[]): ShapeType; queryParser: IQueryParser}, - subject?: ShapeType | QResult, - ): Promise & PatchedQueryPromise { - const propertyLabels = (this as any) - .shape.getUniquePropertyShapes() - .map((propertyShape: PropertyShape) => propertyShape.label); - return (this as any).select(subject as any, (shape: ShapeType) => - propertyLabels.map((label) => (shape as any)[label]), - ) as Promise & PatchedQueryPromise; - } - static update>( this: {new (...args: any[]): ShapeType; queryParser: IQueryParser}, id: string | NodeReferenceValue | QShape, diff --git a/src/test-helpers/query-fixtures.ts b/src/test-helpers/query-fixtures.ts index 670b442..cbf9649 100644 --- a/src/test-helpers/query-fixtures.ts +++ b/src/test-helpers/query-fixtures.ts @@ -31,11 +31,8 @@ export const hasPet = prop('hasPet'); export const guardDogLevel = prop('guardDogLevel'); export const pluralTestProp = prop('pluralTestProp'); export const personClass = type('Person'); -export const employeeClass = type('Employee'); export const petClass = type('Pet'); export const dogClass = type('Dog'); -export const employeeName = prop('employeeName'); -export const employeeDepartment = prop('employeeDepartment'); @linkedShape export class Pet extends Shape { @@ -112,26 +109,6 @@ export class Person extends Shape { } } -@linkedShape -export class Employee extends Person { - static targetClass = employeeClass; - - @literalProperty({path: employeeName, maxCount: 1}) - get name(): string { - return ''; - } - - @objectProperty({path: bestFriend, maxCount: 1, shape: Employee}) - get bestFriend(): Employee { - return null; - } - - @literalProperty({path: employeeDepartment, maxCount: 1}) - get department(): string { - return ''; - } -} - const componentQuery = Person.query((p) => ({name: p.name})); const componentLike = {query: componentQuery}; @@ -195,7 +172,6 @@ export const queryFactories = { p.friends.where((f) => f.name.equals('Jinx').or(f.hobby.equals('Jogging'))), ), selectAll: () => Person.select(), - selectAllProperties: () => Person.selectAll(), selectWhereNameSemmy: () => Person.select().where((p) => p.name.equals('Semmy')), whereAndOrAnd: () => @@ -249,10 +225,6 @@ export const queryFactories = { Person.select((p) => p.friends.select((f) => ({name: f.name, hobby: f.hobby})), ), - subSelectAllProperties: () => - Person.select((p) => p.friends.selectAll()), - subSelectAllPropertiesSingle: () => - Person.select((p) => p.bestFriend.selectAll()), doubleNestedSubSelect: () => Person.select((p) => p.friends.select((p2) => @@ -277,7 +249,6 @@ export const queryFactories = { Person.select((p) => [p.bestFriend, p.friends]), selectShapeAs: () => Person.select((p) => p.firstPet.as(Dog).guardDogLevel), - selectAllEmployeeProperties: () => Employee.selectAll(), selectOne: () => Person.select((p) => p.name).where((p) => p.equals(entity('p1'))).one(), nestedQueries2: () => diff --git a/src/tests/metadata.test.ts b/src/tests/metadata.test.ts index 24a44be..6093ede 100644 --- a/src/tests/metadata.test.ts +++ b/src/tests/metadata.test.ts @@ -1,7 +1,7 @@ import {describe, expect, test} from '@jest/globals'; import {linkedPackage} from '../utils/Package'; import {Shape} from '../shapes/Shape'; -import {literalProperty, objectProperty, LINCD_DATA_ROOT} from '../shapes/SHACL'; +import {literalProperty, LINCD_DATA_ROOT} from '../shapes/SHACL'; import {URI} from '../utils/URI'; import {lincd} from '../ontologies/lincd'; import {shacl} from '../ontologies/shacl'; @@ -62,243 +62,4 @@ describe('Package & Shape Metadata Registration', () => { expect(propertyShape.nodeKind).toEqual(shacl.Literal); expect(propertyShape.parentNodeShape).toBe(MetaPerson.shape); }); - - test('allows override that omits min/max/nodeKind (inherits silently)', () => { - expect(() => { - @linkedShape - class InheritBase extends Shape { - static targetClass = type('InheritBase'); - - @objectProperty({ - path: prop('link'), - shape: InheritBase, - required: true, - maxCount: 1, - nodeKind: shacl.BlankNodeOrIRI, - }) - get link(): InheritBase { - return null; - } - } - - @linkedShape - class InheritChild extends InheritBase { - static targetClass = type('InheritChild'); - - @objectProperty({path: prop('link'), shape: InheritChild}) - get link(): InheritChild { - return null; - } - } - - expect(InheritChild.shape.getPropertyShape('link', false).minCount).toBe(1); - expect(InheritChild.shape.getPropertyShape('link', false).maxCount).toBe(1); - expect(InheritChild.shape.getPropertyShape('link', false).nodeKind).toEqual( - shacl.BlankNodeOrIRI, - ); - }).not.toThrow(); - }); - - test('throws when override lowers minCount', () => { - expect(() => { - @linkedShape - class TightMinBase extends Shape { - static targetClass = type('TightMinBase'); - - @literalProperty({path: prop('label'), minCount: 2}) - get label(): string { - return ''; - } - } - - @linkedShape - class TightMinChild extends TightMinBase { - static targetClass = type('TightMinChild'); - - @literalProperty({path: prop('label'), minCount: 1}) - get label(): string { - return ''; - } - } - - return TightMinChild; - }).toThrow(/minCount cannot be lowered/); - }); - - test('throws when override increases maxCount', () => { - expect(() => { - @linkedShape - class TightMaxBase extends Shape { - static targetClass = type('TightMaxBase'); - - @literalProperty({path: prop('code'), maxCount: 1}) - get code(): string { - return ''; - } - } - - @linkedShape - class TightMaxChild extends TightMaxBase { - static targetClass = type('TightMaxChild'); - - @literalProperty({path: prop('code'), maxCount: 2}) - get code(): string { - return ''; - } - } - - return TightMaxChild; - }).toThrow(/maxCount cannot be increased/); - }); - - test('respects explicit zero minCount and maxCount', () => { - @linkedShape - class ZeroCountShape extends Shape { - static targetClass = type('ZeroCountShape'); - - @literalProperty({path: prop('zeroLimited'), minCount: 0, maxCount: 0}) - get zeroLimited(): string { - return ''; - } - } - - const zeroLimited = ZeroCountShape.shape.getPropertyShape('zeroLimited'); - expect(zeroLimited.minCount).toBe(0); - expect(zeroLimited.maxCount).toBe(0); - }); - - test('throws when override increases explicit zero maxCount', () => { - expect(() => { - @linkedShape - class ZeroMaxBase extends Shape { - static targetClass = type('ZeroMaxBase'); - - @literalProperty({path: prop('locked'), maxCount: 0}) - get locked(): string { - return ''; - } - } - - @linkedShape - class ZeroMaxChild extends ZeroMaxBase { - static targetClass = type('ZeroMaxChild'); - - @literalProperty({path: prop('locked'), maxCount: 1}) - get locked(): string { - return ''; - } - } - - return ZeroMaxChild; - }).toThrow(/maxCount cannot be increased/); - }); - - test('throws when override widens nodeKind', () => { - expect(() => { - @linkedShape - class NodeKindBase extends Shape { - static targetClass = type('NodeKindBase'); - - @objectProperty({ - path: prop('ref'), - shape: NodeKindBase, - nodeKind: shacl.BlankNodeOrIRI, - }) - get ref(): NodeKindBase { - return null; - } - } - - @linkedShape - class NodeKindChild extends NodeKindBase { - static targetClass = type('NodeKindChild'); - - @objectProperty({ - path: prop('ref'), - shape: NodeKindChild, - nodeKind: shacl.IRIOrLiteral, - }) - get ref(): NodeKindChild { - return null; - } - } - - return NodeKindChild; - }).toThrow(/nodeKind cannot be widened/); - }); - - test('allows override that tightens nodeKind', () => { - expect(() => { - @linkedShape - class NodeKindTightenBase extends Shape { - static targetClass = type('NodeKindTightenBase'); - - @objectProperty({ - path: prop('target'), - shape: NodeKindTightenBase, - nodeKind: shacl.BlankNodeOrIRI, - }) - get target(): NodeKindTightenBase { - return null; - } - } - - @linkedShape - class NodeKindTightenChild extends NodeKindTightenBase { - static targetClass = type('NodeKindTightenChild'); - - @objectProperty({ - path: prop('target'), - shape: NodeKindTightenChild, - nodeKind: shacl.IRI, - }) - get target(): NodeKindTightenChild { - return null; - } - } - - expect( - NodeKindTightenChild.shape.getPropertyShape('target', false).nodeKind, - ).toEqual(shacl.IRI); - }).not.toThrow(); - }); - - test('allows mixed override: tighten minCount while maxCount/nodeKind inherit', () => { - expect(() => { - @linkedShape - class MixedInheritBase extends Shape { - static targetClass = type('MixedInheritBase'); - - @objectProperty({ - path: prop('member'), - shape: MixedInheritBase, - minCount: 1, - maxCount: 3, - nodeKind: shacl.BlankNodeOrIRI, - }) - get member(): MixedInheritBase { - return null; - } - } - - @linkedShape - class MixedInheritChild extends MixedInheritBase { - static targetClass = type('MixedInheritChild'); - - @objectProperty({ - path: prop('member'), - shape: MixedInheritChild, - minCount: 2, - }) - get member(): MixedInheritChild { - return null; - } - } - - const member = MixedInheritChild.shape.getPropertyShape('member', false); - expect(member.minCount).toBe(2); - expect(member.maxCount).toBe(3); - expect(member.nodeKind).toEqual(shacl.BlankNodeOrIRI); - }).not.toThrow(); - }); }); diff --git a/src/tests/query.test.ts b/src/tests/query.test.ts index 25cf1c5..8cdfd6b 100644 --- a/src/tests/query.test.ts +++ b/src/tests/query.test.ts @@ -9,7 +9,7 @@ import {UpdateQueryFactory} from '../queries/UpdateQuery'; import {CreateQueryFactory} from '../queries/CreateQuery'; import {DeleteQueryFactory} from '../queries/DeleteQuery'; import {NodeId} from '../queries/MutationQuery'; -import {Dog, Employee, Person, Pet, queryFactories, name as namePath, tmpEntityBase} from '../test-helpers/query-fixtures'; +import {Dog, Person, Pet, queryFactories, name as namePath, tmpEntityBase} from '../test-helpers/query-fixtures'; import {setQueryContext} from '../queries/QueryContext'; class QueryCaptureStore implements IQueryParser { @@ -54,7 +54,6 @@ const store = new QueryCaptureStore(); Person.queryParser = store; Pet.queryParser = store; Dog.queryParser = store; -Employee.queryParser = store; const captureQuery = async (runner: () => Promise) => { store.lastQuery = undefined; @@ -220,37 +219,6 @@ describe('3. Filtering (Where Clauses)', () => { expectSelectQuery(query); }); - test('select all properties of the shape', async () => { - const query = await captureQuery(() => queryFactories.selectAllProperties()); - - expectSelectQuery(query); - const labels = query?.select?.map((path: any) => path?.[0]?.property?.label); - expect(labels).toEqual(expect.arrayContaining([ - 'name', - 'hobby', - 'nickNames', - 'birthDate', - 'isRealPerson', - 'bestFriend', - 'friends', - 'pets', - 'firstPet', - 'pluralTestProp', - ])); - }); - - test('select all uses subclass overrides once', async () => { - const query = await captureQuery(() => queryFactories.selectAllEmployeeProperties()); - - expectSelectQuery(query); - const labels = query?.select?.map((path: any) => path?.[0]?.property?.label); - expect(labels?.filter((label: string) => label === 'name')).toHaveLength(1); - expect(labels?.filter((label: string) => label === 'bestFriend')).toHaveLength(1); - const nameStep = query?.select?.find((path: any) => path?.[0]?.property?.label === 'name'); - expect(nameStep?.[0]?.property?.path?.id).toBe('linked://tmp/props/employeeName'); - expect(labels).toContain('department'); - }); - test('empty select with where', async () => { const query = await captureQuery(() => queryFactories.selectWhereNameSemmy()); @@ -383,34 +351,6 @@ describe('4. Aggregation & Sub-Select', () => { expectSelectQuery(query); }); - test('sub select all properties on a shape set', async () => { - const query = await captureQuery(() => - queryFactories.subSelectAllProperties(), - ); - - expectSelectQuery(query); - expect(query?.select?.[0]?.[0]?.property?.label).toBe('friends'); - const nestedPaths = query?.select?.[0]?.[1] as any[]; - const nestedLabels = nestedPaths?.map((path: any) => - path?.[0]?.property?.label, - ); - expect(nestedLabels).toEqual(expect.arrayContaining(['name', 'hobby'])); - }); - - test('sub select all properties on a single shape', async () => { - const query = await captureQuery(() => - queryFactories.subSelectAllPropertiesSingle(), - ); - - expectSelectQuery(query); - expect(query?.select?.[0]?.[0]?.property?.label).toBe('bestFriend'); - const nestedPaths = query?.select?.[0]?.[1] as any[]; - const nestedLabels = nestedPaths?.map((path: any) => - path?.[0]?.property?.label, - ); - expect(nestedLabels).toEqual(expect.arrayContaining(['name', 'hobby'])); - }); - test('double nested sub select', async () => { const query = await captureQuery(() => queryFactories.doubleNestedSubSelect()); diff --git a/src/tests/query.types.test.ts b/src/tests/query.types.test.ts index f863551..f936f93 100644 --- a/src/tests/query.types.test.ts +++ b/src/tests/query.types.test.ts @@ -205,16 +205,6 @@ describe.skip('query result type inference (compile only)', () => { expectType(first.id); }); - test('select all properties of the shape', () => { - const promise = queryFactories.selectAllProperties(); - type Result = Awaited; - const first = (null as unknown as Result)[0]; - expectType(first.id); - expectType(first.name); - type HasNodeShape = 'nodeShape' extends keyof typeof first ? true : false; - expectType(null as unknown as HasNodeShape); - }); - test('empty select with where', () => { const promise = queryFactories.selectWhereNameSemmy(); type Result = Awaited; @@ -336,24 +326,6 @@ describe.skip('query result type inference (compile only)', () => { expectType(first.friends[0].hobby); }); - test('sub select all properties on a shape set', () => { - const promise = queryFactories.subSelectAllProperties(); - type Result = Awaited; - const first = (null as unknown as Result)[0]; - expectType(first.friends[0].id); - expectType(first.friends[0].name); - expectType(first.friends[0].hobby); - }); - - test('sub select all properties on a single shape', () => { - const promise = queryFactories.subSelectAllPropertiesSingle(); - type Result = Awaited; - const first = (null as unknown as Result)[0]; - expectType(first.bestFriend.id); - expectType(first.bestFriend.name); - expectType(first.bestFriend.hobby); - }); - test('double nested sub select', () => { const promise = queryFactories.doubleNestedSubSelect(); type Result = Awaited;