schema renames, literal union types and uri support

refactor
Laurin Weger 12 hours ago
parent e90a65e4c2
commit 3c004980e0
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 112
      pnpm-lock.yaml
  2. 6
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeHandler.ts
  3. 14
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/README.md
  4. 325
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildConstruct.ts
  5. 152
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildSelect.ts
  6. 140
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildSparqlConstructFromShape.ts
  7. 125
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/common.ts
  8. 9
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/sparqlConstruct.test.ts
  9. 129
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/testShape.schema.ts
  10. 4
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/types.ts
  11. 4
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/catShape.schema.ts
  12. 2
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/catShape.typings.ts
  13. 4
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/personShape.schema.ts
  14. 2
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/personShape.typings.ts
  15. 24
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/testShape.schema.ts
  16. 8
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/testShape.typings.ts
  17. 2
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/shex/catShape.shex
  18. 2
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/shex/personShape.shex
  19. 8
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/shex/testShape.shex
  20. 5
      sdk/ng-sdk-js/ng-shex-orm/package.json
  21. 6
      sdk/ng-sdk-js/ng-shex-orm/src/schema-converter/converter.ts
  22. 115
      sdk/ng-sdk-js/ng-shex-orm/src/schema-converter/transformers/ShexJSchemaTransformer.ts
  23. 7
      sdk/ng-sdk-js/ng-shex-orm/src/schema-converter/transformers/ShexJTypingTransformer.ts
  24. 20
      sdk/ng-sdk-js/ng-shex-orm/src/types.ts
  25. 8
      sdk/ng-sdk-js/ng-signals/src/connector/createSignalObjectForShape.ts
  26. 4
      sdk/ng-sdk-js/ng-signals/src/frontendAdapters/react/useShape.ts
  27. 4
      sdk/ng-sdk-js/ng-signals/src/frontendAdapters/svelte/useShape.svelte.ts
  28. 4
      sdk/ng-sdk-js/ng-signals/src/frontendAdapters/vue/useShape.ts

@ -194,9 +194,6 @@ importers:
'@types/shexj':
specifier: ^2.1.4
version: 2.1.7
copyfiles:
specifier: ^2.4.1
version: 2.4.1
typescript:
specifier: ^5.9.2
version: 5.9.2
@ -1817,9 +1814,6 @@ packages:
resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
engines: {node: '>=10'}
cliui@7.0.4:
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
clone@2.1.2:
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
engines: {node: '>=0.8'}
@ -1899,10 +1893,6 @@ packages:
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
engines: {node: '>=12.13'}
copyfiles@2.4.1:
resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==}
hasBin: true
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@ -2303,10 +2293,6 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-east-asian-width@1.4.0:
resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==}
engines: {node: '>=18'}
@ -2581,9 +2567,6 @@ packages:
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
engines: {node: '>=16'}
isarray@0.0.1:
resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
@ -3025,9 +3008,6 @@ packages:
resolution: {integrity: sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ==}
engines: {node: '>=6.0.0'}
noms@0.0.0:
resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==}
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
@ -3427,9 +3407,6 @@ packages:
readable-stream-node-to-web@1.0.1:
resolution: {integrity: sha512-OGzi2VKLa8H259kAx7BIwuRrXHGcxeHj4RdASSgEGBP9Q2wowdPvBc65upF4Q9O05qWgKqBw1+9PiLTtObl7uQ==}
readable-stream@1.0.34:
resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
@ -3484,10 +3461,6 @@ packages:
remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
require-relative@0.8.7:
resolution: {integrity: sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==}
@ -3684,9 +3657,6 @@ packages:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
string_decoder@0.10.31:
resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
@ -3778,9 +3748,6 @@ packages:
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
through2@2.0.5:
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
tiny-conventional-commits-parser@0.0.1:
resolution: {integrity: sha512-N5+AZWdBeHNSgTIaxvx0+9mFrnW4H1BbjQ84H7i3TuWSkno8Hju886hLaHZhE/hYEKrfrfl/uHurqpZJHDuYGQ==}
@ -4043,10 +4010,6 @@ packages:
uploadthing:
optional: true
untildify@4.0.0:
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
engines: {node: '>=8'}
update-browserslist-db@1.1.3:
resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
hasBin: true
@ -4335,17 +4298,9 @@ packages:
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
xxhash-wasm@1.1.0:
resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
yallist@2.1.2:
resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==}
@ -4355,18 +4310,10 @@ packages:
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yargs-parser@20.2.4:
resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==}
engines: {node: '>=10'}
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
yargs@16.2.0:
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
engines: {node: '>=10'}
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@ -6364,12 +6311,6 @@ snapshots:
cli-boxes@3.0.0: {}
cliui@7.0.4:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
clone@2.1.2: {}
clsx@2.1.1: {}
@ -6433,16 +6374,6 @@ snapshots:
dependencies:
is-what: 4.1.16
copyfiles@2.4.1:
dependencies:
glob: 7.2.3
minimatch: 3.1.2
mkdirp: 1.0.4
noms: 0.0.0
through2: 2.0.5
untildify: 4.0.0
yargs: 16.2.0
core-util-is@1.0.3: {}
cross-fetch@3.2.0(encoding@0.1.13):
@ -6884,8 +6815,6 @@ snapshots:
gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {}
get-east-asian-width@1.4.0: {}
get-intrinsic@1.3.0:
@ -7227,8 +7156,6 @@ snapshots:
dependencies:
is-inside-container: 1.0.0
isarray@0.0.1: {}
isarray@1.0.0: {}
isexe@2.0.0: {}
@ -7877,11 +7804,6 @@ snapshots:
node-version@1.2.0: {}
noms@0.0.0:
dependencies:
inherits: 2.0.4
readable-stream: 1.0.34
normalize-path@3.0.0: {}
npm-run-path@6.0.0:
@ -8279,13 +8201,6 @@ snapshots:
readable-stream-node-to-web@1.0.1: {}
readable-stream@1.0.34:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 0.0.1
string_decoder: 0.10.31
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
@ -8386,8 +8301,6 @@ snapshots:
mdast-util-to-markdown: 2.1.2
unified: 11.0.5
require-directory@2.1.1: {}
require-relative@0.8.7: {}
resolve-from@4.0.0: {}
@ -8645,8 +8558,6 @@ snapshots:
get-east-asian-width: 1.4.0
strip-ansi: 7.1.2
string_decoder@0.10.31: {}
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
@ -8776,11 +8687,6 @@ snapshots:
dependencies:
any-promise: 1.3.0
through2@2.0.5:
dependencies:
readable-stream: 2.3.8
xtend: 4.0.2
tiny-conventional-commits-parser@0.0.1: {}
tiny-inflate@1.0.3: {}
@ -8990,8 +8896,6 @@ snapshots:
ofetch: 1.4.1
ufo: 1.6.1
untildify@4.0.0: {}
update-browserslist-db@1.1.3(browserslist@4.26.0):
dependencies:
browserslist: 4.26.0
@ -9364,32 +9268,16 @@ snapshots:
xmlchars@2.2.0: {}
xtend@4.0.2: {}
xxhash-wasm@1.1.0: {}
y18n@5.0.8: {}
yallist@2.1.2: {}
yallist@3.1.1: {}
yallist@4.0.0: {}
yargs-parser@20.2.4: {}
yargs-parser@21.1.1: {}
yargs@16.2.0:
dependencies:
cliui: 7.0.4
escalade: 3.2.0
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 20.2.4
yocto-queue@0.1.0: {}
yocto-queue@1.2.1: {}

@ -1,6 +1,6 @@
import * as shapeManager from "./shapeManager";
import type { WasmConnection, Diff, Scope } from "./types";
import type { ShapeType, OrmBase } from "@nextgraph-monorepo/ng-shex-orm";
import type { ShapeType, BaseType } from "@nextgraph-monorepo/ng-shex-orm";
import type { Person } from "../../shapes/ldo/personShape.typings";
import type { Cat } from "../../shapes/ldo/catShape.typings";
import type { TestObject } from "../../shapes/ldo/testShape.typings";
@ -17,7 +17,7 @@ interface WasmMessage {
connectionId: string;
diff?: Diff;
shapeType?: ShapeType<any>;
initialData?: OrmBase;
initialData?: BaseType;
}
export const mockTestObject = {
@ -78,7 +78,7 @@ const mockShapeObject2 = {
// Single BroadcastChannel for wasm-land side
const communicationChannel = new BroadcastChannel("shape-manager");
function getInitialObjectByShapeId<T extends OrmBase>(shapeId?: string): T {
function getInitialObjectByShapeId<T extends BaseType>(shapeId?: string): T {
if (shapeId?.includes("TestObject")) return mockTestObject as unknown as T;
if (shapeId?.includes("Person")) return mockShapeObject1 as unknown as T;
if (shapeId?.includes("Cat")) return mockShapeObject2 as unknown as T;

@ -1,14 +0,0 @@
# SPARQL builders
Utilities to build SPARQL SELECT and CONSTRUCT queries from a ShapeConstraint structure.
Exports:
- buildSelectQuery(shape, options)
- buildConstructQuery(shape, options)
Options:
- prefixes: Record<prefix, IRI>
- graph: named graph IRI or CURIE
- includeOptionalForMinZero: wrap min=0 predicates in OPTIONAL (default true)

@ -1,17 +1,4 @@
import type {
BuildContext,
PredicateConstraint,
ShapeConstraint,
SparqlBuildOptions,
} from "./common";
import {
predicateToSparql,
prefixesToText,
toIriOrCurie,
uniqueVar,
valuesBlock,
varToken,
} from "./common";
import type { Predicate, Shape, Schema } from "@nextgraph-monorepo/ng-shex-orm";
/**
* Build a SPARQL CONSTRUCT query from a ShapeConstraint definition.
@ -19,131 +6,215 @@ import {
* but still appear in the CONSTRUCT template so that matched triples are constructed.
*/
export function buildConstructQuery(
shape: ShapeConstraint,
options?: SparqlBuildOptions,
shape: Shape,
schema: Schema,
options?: SparqlBuildOptions
): string {
const ctx: BuildContext = { usedVars: new Set<string>() };
const prefixes = prefixesToText(options?.prefixes);
const subject = toIriOrCurie(shape.subject);
const templateLines: string[] = [];
const whereLines: string[] = [];
const postFilters: string[] = [];
const valuesBlocks: string[] = [];
const rootVar =
subject.startsWith("?") || subject.startsWith("$")
? subject
: uniqueVar(ctx, "s");
if (!subject.startsWith("?") && !subject.startsWith("$")) {
valuesBlocks.push(valuesBlock(rootVar, [subject] as any));
}
const predicates = Array.isArray(shape.predicates)
? shape.predicates
: [...shape.predicates];
for (const pred of predicates) {
addConstructPattern(
ctx,
pred,
rootVar,
templateLines,
whereLines,
postFilters,
valuesBlocks,
options,
const ctx: BuildContext = { usedVars: new Set<string>() };
const prefixes = prefixesToText(options?.prefixes);
const subject = shape.iri;
const templateLines: string[] = [];
const whereLines: string[] = [];
const postFilters: string[] = [];
const valuesBlocks: string[] = [];
const rootVar =
subject.startsWith("?") || subject.startsWith("$")
? subject
: uniqueVar(ctx, "s");
if (!subject.startsWith("?") && !subject.startsWith("$")) {
valuesBlocks.push(valuesBlock(rootVar, [subject] as any));
}
const predicates = Array.isArray(shape.predicates)
? shape.predicates
: [...shape.predicates];
for (const pred of predicates) {
addConstructPattern(
ctx,
pred,
rootVar,
templateLines,
whereLines,
postFilters,
valuesBlocks,
options
);
}
const graphWrap = (body: string) =>
options?.graph
? `GRAPH ${toIriOrCurie(options.graph)} {\n${body}\n}`
: body;
const where = [
...valuesBlocks,
graphWrap(whereLines.join("\n")),
...postFilters,
]
.filter(Boolean)
.join("\n");
const template = templateLines.join("\n");
return [prefixes, `CONSTRUCT {`, template, `} WHERE {`, where, `}`].join(
"\n"
);
}
const graphWrap = (body: string) =>
options?.graph
? `GRAPH ${toIriOrCurie(options.graph)} {\n${body}\n}`
: body;
const where = [
...valuesBlocks,
graphWrap(whereLines.join("\n")),
...postFilters,
]
.filter(Boolean)
.join("\n");
const template = templateLines.join("\n");
return [prefixes, `CONSTRUCT {`, template, `} WHERE {`, where, `}`].join(
"\n",
);
}
function addConstructPattern(
ctx: BuildContext,
pred: PredicateConstraint,
subjectVar: string,
template: string[],
where: string[],
postFilters: string[],
valuesBlocks: string[],
options?: SparqlBuildOptions,
ctx: BuildContext,
pred: Predicate,
subjectVar: string,
template: string[],
where: string[],
postFilters: string[],
valuesBlocks: string[],
options?: SparqlBuildOptions
) {
const p = predicateToSparql(pred.uri);
const objVar = uniqueVar(ctx, pred.displayName || "o");
const objTerm =
pred.type === "nested" &&
pred.nested?.subject &&
!pred.nested.subject.match(/^\?|^\$/)
? toIriOrCurie(pred.nested.subject)
: objVar;
const triple = `${subjectVar} ${p} ${objTerm} .`;
const isOptional =
(pred.min ?? 0) === 0 && (options?.includeOptionalForMinZero ?? true);
const p = `<${pred.predicateUri}>`;
const objVar = uniqueVar(ctx, pred.readablePredicate);
const triple = `${subjectVar} ${p} ${objTerm} .`;
const isOptional =
(pred.minCardinality ?? 0) === 0 &&
(options?.includeOptionalForMinZero ?? true);
if (pred.type === "nested" && pred.nestedShape) {
template.push(triple);
const nestedBody: string[] = [triple];
const nestedPreds = pred.nestedShape.predicates;
for (const n of nestedPreds) {
addConstructPattern(
ctx,
n,
objTerm,
template,
nestedBody,
postFilters,
valuesBlocks,
options
);
}
const block = nestedBody.join("\n");
where.push(isOptional ? `OPTIONAL {\n${block}\n}` : block);
return;
}
if (pred.type === "nested" && pred.nested) {
// Non-nested
template.push(triple);
const nestedBody: string[] = [triple];
const nestedPreds = Array.isArray(pred.nested.predicates)
? pred.nested.predicates
: [...pred.nested.predicates];
for (const n of nestedPreds) {
addConstructPattern(
ctx,
n,
objTerm,
template,
nestedBody,
postFilters,
valuesBlocks,
options,
);
const blockLines: string[] = [triple];
if (pred.type === "literal" && pred.literalValue !== undefined) {
if (Array.isArray(pred.literalValue)) {
valuesBlocks.push(valuesBlock(objVar, pred.literalValue as any[]));
} else {
const lit =
typeof pred.literalValue === "string" ||
typeof pred.literalValue === "number" ||
typeof pred.literalValue === "boolean"
? pred.literalValue
: String(pred.literalValue);
postFilters.push(
`FILTER(${objVar} = ${typeof lit === "string" ? `"${String(lit).replace(/"/g, '\\"')}"` : lit})`
);
}
}
const block = nestedBody.join("\n");
const block = blockLines.join("\n");
where.push(isOptional ? `OPTIONAL {\n${block}\n}` : block);
return;
}
// Non-nested
template.push(triple);
const blockLines: string[] = [triple];
if (pred.type === "literal" && pred.literalValue !== undefined) {
if (Array.isArray(pred.literalValue)) {
valuesBlocks.push(valuesBlock(objVar, pred.literalValue as any[]));
} else {
const lit =
typeof pred.literalValue === "string" ||
typeof pred.literalValue === "number" ||
typeof pred.literalValue === "boolean"
? pred.literalValue
: String(pred.literalValue);
postFilters.push(
`FILTER(${objVar} = ${typeof lit === "string" ? `"${String(lit).replace(/"/g, '\\"')}"` : lit})`,
);
}
export type LiteralKind =
| "number"
| "string"
| "boolean"
| "nested"
| "literal";
export interface SparqlBuildOptions {
prefixes?: Record<string, string>;
graph?: string; // IRI of the named graph to query, if any
includeOptionalForMinZero?: boolean; // default true
}
export const defaultPrefixes: Record<string, string> = {
xsd: "http://www.w3.org/2001/XMLSchema#",
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
};
export function prefixesToText(prefixes?: Record<string, string>): string {
const all = { ...defaultPrefixes, ...(prefixes ?? {}) };
return Object.entries(all)
.map(([p, iri]) => `PREFIX ${p}: <${iri}>`)
.join("\n");
}
export function safeVarName(name: string): string {
const base = name
.replace(/[^a-zA-Z0-9_]/g, "_")
.replace(/^([0-9])/, "_$1")
.slice(0, 60);
return base || "v";
}
export function varToken(name: string): string {
const n =
name.startsWith("?") || name.startsWith("$") ? name.slice(1) : name;
return `?${safeVarName(n)}`;
}
export function formatLiteral(value: string | number | boolean): string {
if (typeof value === "number") return String(value);
if (typeof value === "boolean") return value ? "true" : "false";
// default string literal
const escaped = value.replace(/"/g, '\\"');
return `"${escaped}"`;
}
export function formatTermForValues(value: string | number | boolean): string {
if (typeof value === "number" || typeof value === "boolean")
return formatLiteral(value);
// strings: detect IRI or CURIE and keep raw; otherwise quote
const v = value.trim();
const looksLikeIri = v.startsWith("<") && v.endsWith(">");
const looksLikeHttp = v.includes("://");
const looksLikeCurie =
/^[A-Za-z_][A-Za-z0-9_-]*:.+$/u.test(v) && !looksLikeHttp;
if (looksLikeIri || looksLikeHttp || looksLikeCurie) {
return looksLikeHttp ? `<${v}>` : v;
}
}
return formatLiteral(v);
}
export function valuesBlock(
varName: string,
values: Array<string | number | boolean>
): string {
const rendered = values.map(formatTermForValues).join(" ");
return `VALUES ${varName} { ${rendered} }`;
}
const block = blockLines.join("\n");
where.push(isOptional ? `OPTIONAL {\n${block}\n}` : block);
export interface BuildContext {
// Tracks used variable names to avoid collisions
usedVars: Set<string>;
}
export function uniqueVar(ctx: BuildContext, base: string): string {
let candidate = varToken(base);
if (!ctx.usedVars.has(candidate)) {
ctx.usedVars.add(candidate);
return candidate;
}
let i = 2;
while (ctx.usedVars.has(`${candidate}_${i}`)) i++;
const unique = `${candidate}_${i}`;
ctx.usedVars.add(unique);
return unique;
}
export default buildConstructQuery;

@ -1,152 +0,0 @@
import type {
BuildContext,
PredicateConstraint,
ShapeConstraint,
SparqlBuildOptions,
} from "./common";
import {
predicateToSparql,
prefixesToText,
toIriOrCurie,
uniqueVar,
valuesBlock,
varToken,
} from "./common";
/**
* Build a SPARQL SELECT query from a ShapeConstraint definition.
* The query matches the shape subject and constraints; optional predicates (min=0) are wrapped in OPTIONAL.
*/
export function buildSelectQuery(
shape: ShapeConstraint,
options?: SparqlBuildOptions,
): string {
const ctx: BuildContext = { usedVars: new Set<string>() };
const prefixes = prefixesToText(options?.prefixes);
const subject = toIriOrCurie(shape.subject);
const selectVars: string[] = [];
const whereLines: string[] = [];
const postFilters: string[] = [];
const valuesBlocks: string[] = [];
// ensure a consistent root variable when subject is a variable
const rootVar =
subject.startsWith("?") || subject.startsWith("$")
? subject
: uniqueVar(ctx, "s");
if (!subject.startsWith("?") && !subject.startsWith("$")) {
// bind fixed subject via VALUES for portability
valuesBlocks.push(valuesBlock(rootVar, [subject] as any));
}
const predicates = Array.isArray(shape.predicates)
? shape.predicates
: [...shape.predicates];
for (const pred of predicates) {
addPredicatePattern(
ctx,
pred,
rootVar,
whereLines,
selectVars,
postFilters,
valuesBlocks,
options,
);
}
const graphWrap = (body: string) =>
options?.graph
? `GRAPH ${toIriOrCurie(options.graph)} {\n${body}\n}`
: body;
const where = [
...valuesBlocks,
graphWrap(whereLines.join("\n")),
...postFilters,
]
.filter(Boolean)
.join("\n");
const select = selectVars.length ? selectVars.join(" ") : "*";
return [prefixes, `SELECT ${select} WHERE {`, where, `}`].join("\n");
}
function addPredicatePattern(
ctx: BuildContext,
pred: PredicateConstraint,
subjectVar: string,
where: string[],
selectVars: string[],
postFilters: string[],
valuesBlocks: string[],
options?: SparqlBuildOptions,
) {
const p = predicateToSparql(pred.uri);
const objVar = uniqueVar(ctx, pred.displayName || "o");
const objTerm =
pred.type === "nested" &&
pred.nested?.subject &&
!pred.nested.subject.match(/^\?|^\$/)
? toIriOrCurie(pred.nested.subject)
: objVar;
const triple = `${subjectVar} ${p} ${objTerm} .`;
const isOptional =
(pred.min ?? 0) === 0 && (options?.includeOptionalForMinZero ?? true);
if (pred.type === "nested" && pred.nested) {
// For nested, we select the nested object var and then recurse
if (objTerm === objVar) selectVars.push(objVar);
const nestedBody: string[] = [triple];
const nestedPreds = Array.isArray(pred.nested.predicates)
? pred.nested.predicates
: [...pred.nested.predicates];
for (const n of nestedPreds) {
addPredicatePattern(
ctx,
n,
objTerm,
nestedBody,
selectVars,
postFilters,
valuesBlocks,
options,
);
}
const block = nestedBody.join("\n");
where.push(isOptional ? `OPTIONAL {\n${block}\n}` : block);
return;
}
// Non-nested: literals or IRIs
selectVars.push(objVar);
const blockLines: string[] = [triple];
if (pred.type === "literal" && pred.literalValue !== undefined) {
if (Array.isArray(pred.literalValue)) {
// VALUES block for IN-like matching
valuesBlocks.push(valuesBlock(objVar, pred.literalValue as any[]));
} else {
// simple equality filter
const lit =
typeof pred.literalValue === "string" ||
typeof pred.literalValue === "number" ||
typeof pred.literalValue === "boolean"
? pred.literalValue
: String(pred.literalValue);
postFilters.push(
`FILTER(${objVar} = ${typeof lit === "string" ? `"${String(lit).replace(/"/g, '\\"')}"` : lit})`,
);
}
}
const block = blockLines.join("\n");
where.push(isOptional ? `OPTIONAL {\n${block}\n}` : block);
}
export default buildSelectQuery;

@ -0,0 +1,140 @@
import type { Predicate, Shape, Schema } from "@nextgraph-monorepo/ng-shex-orm";
export const buildConstructQuery = ({
schema,
shapeId,
}: {
schema: Schema;
shapeId: keyof Schema;
}): string => {
const rootShape = schema[shapeId];
const constructStatements: {
s: string;
p: string;
o: string;
optional: boolean;
literals: Predicate["literalValue"];
}[] = [];
const idToVarName: Record<string, string> = {};
const getVarNameFor = (id: string) => {
const currentName = idToVarName[id];
if (currentName) return currentName;
const newVar = `o${Object.entries(idToVarName).length + 1}`;
idToVarName[id] = newVar;
return newVar;
};
// Create s,p,o records where subject and object var names are mapped to shape or predicate ids.
const addTriples = (shape: Shape) => {
const predicates = shape.predicates;
const shapeId = shape.iri;
for (const pred of predicates) {
const subjectVarName = getVarNameFor(shapeId);
if (pred.type === "nested") {
if (typeof pred.nestedShape !== "string")
throw new Error("Nested shapes must be by reference");
// If a name for this shape was assigned already, it's triples have been added
// and we don't have to recurse.
const shapeAlreadyRegistered = !!idToVarName[pred.nestedShape];
const shapeVarName = getVarNameFor(pred.nestedShape);
constructStatements.push({
s: `?${subjectVarName}`,
p: `<${pred.predicateUri}>`,
o: `?${shapeVarName}`,
optional: pred.minCardinality < 1,
literals: pred.literalValue,
// TODO: eitherOf ?
});
if (!shapeAlreadyRegistered)
addTriples(schema[pred.nestedShape]);
} else {
const objVarName = getVarNameFor(
shapeId + "__separator__" + pred.predicateUri
);
constructStatements.push({
s: `?${subjectVarName}`,
p: `<${pred.predicateUri}>`,
o: `?${objVarName}`,
optional: pred.minCardinality < 1,
literals: pred.literalValue,
// TODO: eitherOf ?
});
}
}
};
addTriples(rootShape);
const construct = `CONSTRUCT {
${constructStatements.map(({ s, p, o }) => ` ${s} ${p} ${o} .\n`).join("")} }`;
const statementToWhere = ({
s,
p,
o,
optional,
}: {
s: string;
p: string;
o: string;
optional: boolean;
}) => {
if (optional) return ` OPTIONAL { ${s} ${p} ${o} . }\n`;
else return ` ${s} ${p} ${o} .\n`;
};
const literalToSparqlFormat = (
literal: string | number | boolean
): string => {
if (typeof literal === "number") return String(literal);
if (typeof literal === "boolean") return literal ? "true" : "false";
if (typeof literal === "string") {
return isIri(literal)
? `<${literal}>`
: `"${escapeString(literal)}"`;
}
return `"${String(literal)}"`;
};
// Filters for optional values.
const filters = constructStatements
.filter((statement) => statement.literals !== undefined)
.map((statement) => {
const vals = arrayOf(statement.literals!);
if (vals.length === 0) return "";
if (vals.length === 1) {
return ` FILTER(${statement.o} = ${literalToSparqlFormat(vals[0]!)})\n`;
}
const list = vals.map(literalToSparqlFormat).join(", ");
return ` FILTER(${statement.o} IN (${list}))\n`;
})
.join("");
const where = `WHERE {
${constructStatements.map(statementToWhere).join("")}
${filters}
}`;
return `${construct}\n${where}`;
};
const arrayOf = <T extends any>(arrayOrLiteral: T | T[]) => {
if (typeof arrayOrLiteral === "undefined" || arrayOrLiteral === null)
return [];
if (Array.isArray(arrayOrLiteral)) return arrayOrLiteral;
return [arrayOrLiteral];
};
const isIri = (str: string) => /^[a-zA-Z][a-zA-Z0-9+.-]{1,7}:/.test(str);
const escapeString = (str: string) => str.replace(/["\\]/g, "\\$&");

@ -1,125 +0,0 @@
/**
* Shared helpers and types to build SPARQL queries from ShapeConstraint
*/
export type LiteralKind =
| "number"
| "string"
| "boolean"
| "nested"
| "literal";
export interface PredicateConstraint {
displayName: string;
uri: string;
type: LiteralKind;
literalValue?: number | string | boolean | number[] | string[];
nested?: ShapeConstraint;
min: number;
max: number;
currentCount: number;
}
export interface ShapeConstraint {
subject: string;
// In upstream code this is typed as a 1-length tuple; we normalize to an array here
predicates: PredicateConstraint[] | [PredicateConstraint];
}
export interface SparqlBuildOptions {
prefixes?: Record<string, string>;
graph?: string; // IRI of the named graph to query, if any
includeOptionalForMinZero?: boolean; // default true
}
export const defaultPrefixes: Record<string, string> = {
xsd: "http://www.w3.org/2001/XMLSchema#",
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
};
export function prefixesToText(prefixes?: Record<string, string>): string {
const all = { ...defaultPrefixes, ...(prefixes ?? {}) };
return Object.entries(all)
.map(([p, iri]) => `PREFIX ${p}: <${iri}>`)
.join("\n");
}
export function toIriOrCurie(term: string): string {
// variable
if (term.startsWith("?") || term.startsWith("$")) return term;
// blank node
if (term.startsWith("_:")) return term;
// full IRI
if (term.includes("://")) return `<${term}>`;
// fallback: assume CURIE or already-angled
if (term.startsWith("<") && term.endsWith(">")) return term;
return term; // CURIE, caller must ensure prefix provided
}
export function predicateToSparql(uri: string): string {
// Allow CURIEs or IRIs
return toIriOrCurie(uri);
}
export function safeVarName(name: string): string {
const base = name
.replace(/[^a-zA-Z0-9_]/g, "_")
.replace(/^([0-9])/, "_$1")
.slice(0, 60);
return base || "v";
}
export function varToken(name: string): string {
const n = name.startsWith("?") || name.startsWith("$") ? name.slice(1) : name;
return `?${safeVarName(n)}`;
}
export function formatLiteral(value: string | number | boolean): string {
if (typeof value === "number") return String(value);
if (typeof value === "boolean") return value ? "true" : "false";
// default string literal
const escaped = value.replace(/"/g, '\\"');
return `"${escaped}"`;
}
export function formatTermForValues(value: string | number | boolean): string {
if (typeof value === "number" || typeof value === "boolean")
return formatLiteral(value);
// strings: detect IRI or CURIE and keep raw; otherwise quote
const v = value.trim();
const looksLikeIri = v.startsWith("<") && v.endsWith(">");
const looksLikeHttp = v.includes("://");
const looksLikeCurie =
/^[A-Za-z_][A-Za-z0-9_-]*:.+$/u.test(v) && !looksLikeHttp;
if (looksLikeIri || looksLikeHttp || looksLikeCurie) {
return looksLikeHttp ? `<${v}>` : v;
}
return formatLiteral(v);
}
export function valuesBlock(
varName: string,
values: Array<string | number | boolean>,
): string {
const rendered = values.map(formatTermForValues).join(" ");
return `VALUES ${varName} { ${rendered} }`;
}
export interface BuildContext {
// Tracks used variable names to avoid collisions
usedVars: Set<string>;
}
export function uniqueVar(ctx: BuildContext, base: string): string {
let candidate = varToken(base);
if (!ctx.usedVars.has(candidate)) {
ctx.usedVars.add(candidate);
return candidate;
}
let i = 2;
while (ctx.usedVars.has(`${candidate}_${i}`)) i++;
const unique = `${candidate}_${i}`;
ctx.usedVars.add(unique);
return unique;
}

@ -0,0 +1,9 @@
import { buildConstructQuery } from "./buildSparqlConstructFromShape.ts";
import { testShapeSchema } from "./testShape.schema.ts";
console.log(
buildConstructQuery({
schema: testShapeSchema,
shapeId: "http://example.org/TestObject",
})
);

@ -0,0 +1,129 @@
import type { Schema } from "@nextgraph-monorepo/ng-shex-orm";
/**
* =============================================================================
* testShapeSchema: Schema for testShape
* =============================================================================
*/
export const testShapeSchema: Schema = {
"http://example.org/TestObject": {
iri: "http://example.org/TestObject",
predicates: [
{
type: "literal",
literalValue: ["TestObject"],
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "type",
extra: true,
},
{
type: "string",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/stringValue",
readablePredicate: "stringValue",
},
{
type: "number",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/numValue",
readablePredicate: "numValue",
},
{
type: "boolean",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/boolValue",
readablePredicate: "boolValue",
},
{
type: "number",
maxCardinality: -1,
minCardinality: 0,
predicateUri: "http://example.org/arrayValue",
readablePredicate: "arrayValue",
},
{
type: "nested",
nestedShape:
"http://example.org/TestObject||http://example.org/objectValue",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/objectValue",
readablePredicate: "objectValue",
},
{
type: "nested",
nestedShape:
"http://example.org/TestObject||http://example.org/anotherObject",
maxCardinality: -1,
minCardinality: 0,
predicateUri: "http://example.org/anotherObject",
readablePredicate: "anotherObject",
},
{
type: "eitherOf",
eitherOf: [
{
type: "string",
},
{
type: "number",
},
],
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/numOrStr",
readablePredicate: "numOrStr",
},
],
},
"http://example.org/TestObject||http://example.org/objectValue": {
iri: "http://example.org/TestObject||http://example.org/objectValue",
predicates: [
{
type: "string",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/nestedString",
readablePredicate: "nestedString",
},
{
type: "number",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/nestedNum",
readablePredicate: "nestedNum",
},
{
type: "number",
maxCardinality: -1,
minCardinality: 0,
predicateUri: "http://example.org/nestedArray",
readablePredicate: "nestedArray",
},
],
},
"http://example.org/TestObject||http://example.org/anotherObject": {
iri: "http://example.org/TestObject||http://example.org/anotherObject",
predicates: [
{
type: "string",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/prop1",
readablePredicate: "prop1",
},
{
type: "number",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/prop2",
readablePredicate: "prop2",
},
],
},
};

@ -1,4 +1,4 @@
import type { ShapeType, OrmBase } from "@nextgraph-monorepo/ng-shex-orm";
import type { ShapeType, BaseType } from "@nextgraph-monorepo/ng-shex-orm";
import type { Patch } from "@nextgraph-monorepo/ng-signals";
/** The Scope of a shape request */
@ -10,7 +10,7 @@ export type Diff = Patch[];
export type ObjectState = object;
/** A connection established between wasm-land and js-land for subscription of a shape. */
export type WasmConnection<T extends OrmBase = OrmBase> = {
export type WasmConnection<T extends BaseType = BaseType> = {
id: string;
shape: ShapeType<T>;
state: ObjectState;

@ -11,7 +11,7 @@ export const catShapeSchema: Schema = {
predicates: [
{
type: "literal",
literalValue: ["Cat"],
literalValue: ["http://example.org/Cat"],
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
@ -40,7 +40,7 @@ export const catShapeSchema: Schema = {
},
{
type: "nested",
nestedSchema: "http://example.org/Cat||http://example.org/address",
nestedShape: "http://example.org/Cat||http://example.org/address",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/address",

@ -14,7 +14,7 @@ export interface Cat {
/**
* Original IRI: http://www.w3.org/1999/02/22-rdf-syntax-ns#type
*/
type: "Cat";
type: string;
/**
* Original IRI: http://example.org/name
*/

@ -11,7 +11,7 @@ export const personShapeSchema: Schema = {
predicates: [
{
type: "literal",
literalValue: ["Person"],
literalValue: ["http://example.org/Person"],
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
@ -26,7 +26,7 @@ export const personShapeSchema: Schema = {
},
{
type: "nested",
nestedSchema: "http://example.org/Person||http://example.org/address",
nestedShape: "http://example.org/Person||http://example.org/address",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/address",

@ -14,7 +14,7 @@ export interface Person {
/**
* Original IRI: http://www.w3.org/1999/02/22-rdf-syntax-ns#type
*/
type: "Person";
type: string;
/**
* Original IRI: http://example.org/name
*/

@ -11,7 +11,7 @@ export const testShapeSchema: Schema = {
predicates: [
{
type: "literal",
literalValue: ["TestObject"],
literalValue: ["http://example.org/TestObject"],
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
@ -48,7 +48,7 @@ export const testShapeSchema: Schema = {
},
{
type: "nested",
nestedSchema:
nestedShape:
"http://example.org/TestObject||http://example.org/objectValue",
maxCardinality: 1,
minCardinality: 1,
@ -57,7 +57,7 @@ export const testShapeSchema: Schema = {
},
{
type: "nested",
nestedSchema:
nestedShape:
"http://example.org/TestObject||http://example.org/anotherObject",
maxCardinality: -1,
minCardinality: 0,
@ -65,12 +65,28 @@ export const testShapeSchema: Schema = {
readablePredicate: "anotherObject",
},
{
type: "string",
type: "eitherOf",
eitherOf: [
{
type: "string",
},
{
type: "number",
},
],
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/numOrStr",
readablePredicate: "numOrStr",
},
{
type: "literal",
literalValue: ["lit1", "lit2"],
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/lit1Or2",
readablePredicate: "lit1Or2",
},
],
},
"http://example.org/TestObject||http://example.org/objectValue": {

@ -14,7 +14,7 @@ export interface TestObject {
/**
* Original IRI: http://www.w3.org/1999/02/22-rdf-syntax-ns#type
*/
type: "TestObject";
type: string;
/**
* Original IRI: http://example.org/stringValue
*/
@ -69,5 +69,9 @@ export interface TestObject {
/**
* Original IRI: http://example.org/numOrStr
*/
numOrStr: string;
numOrStr: string | number;
/**
* Original IRI: http://example.org/lit1Or2
*/
lit1Or2: "lit1" | "lit2";
}

@ -2,7 +2,7 @@ PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
ex:Cat {
a ["Cat"] ;
a [ ex:Cat ] ;
ex:name xsd:string ;
ex:age xsd:integer ;
ex:numberOfHomes xsd:integer ;

@ -2,7 +2,7 @@ PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
ex:Person {
a ["Person"] ;
a [ ex:Person ] ;
ex:name xsd:string ;
ex:address {
ex:street xsd:string ;

@ -2,7 +2,7 @@ PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
ex:TestObject EXTRA a {
a ["TestObject"] ;
a [ ex:TestObject ] ;
ex:stringValue xsd:string ;
ex:numValue xsd:integer ;
ex:boolValue xsd:boolean ;
@ -13,9 +13,9 @@ ex:TestObject EXTRA a {
ex:nestedArray xsd:integer* ;
} ;
ex:anotherObject {
ex:prop1 xsd:string;
ex:prop1 xsd:string ;
ex:prop2 xsd:integer ;
} * ;
ex:numOrStr xsd:string;
# TODO: ShapeOr -- | ex:numOrStr xsd:integer
ex:numOrStr xsd:string OR xsd:integer ;
ex:lit1Or2 ["lit1" "lit2"] ;
}

@ -5,7 +5,7 @@
"type": "module",
"main": "src/index.ts",
"bin": {
"ldo": "./dist/cli.js"
"ldo": "./src/cli.ts"
},
"scripts": {
"build": "pnpm run build:ts",
@ -59,6 +59,9 @@
"src"
],
"publishConfig": {
"bin": {
"ldo": "./dist/cli.js"
},
"access": "public",
"main": "./dist/index.js",
"types": "./dist/index.d.ts"

@ -80,7 +80,7 @@ function flattenSchema(shapes: Shape[]): ShapeSchema {
// Find nested, unflattened (i.e. anonymous) schemas in properties.
const nestedSchemaPredicates = shape.predicates.filter(
(pred) =>
pred.type === "nested" && typeof pred.nestedSchema === "object"
pred.type === "nested" && typeof pred.nestedShape === "object"
);
for (const pred of nestedSchemaPredicates) {
@ -89,12 +89,12 @@ function flattenSchema(shapes: Shape[]): ShapeSchema {
// Recurse
const flattened = flattenSchema([
{
...(pred.nestedSchema as Shape),
...(pred.nestedShape as Shape),
iri: newId,
},
]);
// Replace the nested schema with its new id.
pred.nestedSchema = newId;
pred.nestedShape = newId;
schema = { ...schema, ...flattened };
}

@ -1,6 +1,61 @@
import ShexJTraverser from "@ldo/traverser-shexj";
import { SchemaProperty, SchemaValue, Shape } from "../../types.ts";
import { ObjectLiteral } from "../../ShexJTypes.ts";
import type { Predicate, DataType, Shape } from "../../types.ts";
import type { ObjectLiteral } from "../../ShexJTypes.ts";
const rdfDataTypeToBasic = (dataType: string) => {
switch (dataType) {
case "http://www.w3.org/2001/XMLSchema#string":
case "http://www.w3.org/2001/XMLSchema#ENTITIES":
case "http://www.w3.org/2001/XMLSchema#ENTITY":
case "http://www.w3.org/2001/XMLSchema#ID":
case "http://www.w3.org/2001/XMLSchema#IDREF":
case "http://www.w3.org/2001/XMLSchema#IDREFS":
case "http://www.w3.org/2001/XMLSchema#language":
case "http://www.w3.org/2001/XMLSchema#Name":
case "http://www.w3.org/2001/XMLSchema#NCName":
case "http://www.w3.org/2001/XMLSchema#NMTOKEN":
case "http://www.w3.org/2001/XMLSchema#NMTOKENS":
case "http://www.w3.org/2001/XMLSchema#normalizedString":
case "http://www.w3.org/2001/XMLSchema#QName":
case "http://www.w3.org/2001/XMLSchema#token":
return "string";
case "http://www.w3.org/2001/XMLSchema#date":
case "http://www.w3.org/2001/XMLSchema#dateTime":
case "http://www.w3.org/2001/XMLSchema#duration":
case "http://www.w3.org/2001/XMLSchema#gDay":
case "http://www.w3.org/2001/XMLSchema#gMonth":
case "http://www.w3.org/2001/XMLSchema#gMonthDay":
case "http://www.w3.org/2001/XMLSchema#gYear":
case "http://www.w3.org/2001/XMLSchema#gYearMonth":
case "http://www.w3.org/2001/XMLSchema#time":
return "string";
case "http://www.w3.org/2001/XMLSchema#byte":
case "http://www.w3.org/2001/XMLSchema#decimal":
case "http://www.w3.org/2001/XMLSchema#double":
case "http://www.w3.org/2001/XMLSchema#float":
case "http://www.w3.org/2001/XMLSchema#int":
case "http://www.w3.org/2001/XMLSchema#integer":
case "http://www.w3.org/2001/XMLSchema#long":
case "http://www.w3.org/2001/XMLSchema#negativeInteger":
case "http://www.w3.org/2001/XMLSchema#nonNegativeInteger":
case "http://www.w3.org/2001/XMLSchema#nonPositiveInteger":
case "http://www.w3.org/2001/XMLSchema#positiveInteger":
case "http://www.w3.org/2001/XMLSchema#short":
case "http://www.w3.org/2001/XMLSchema#unsignedLong":
case "http://www.w3.org/2001/XMLSchema#unsignedInt":
case "http://www.w3.org/2001/XMLSchema#unsignedShort":
case "http://www.w3.org/2001/XMLSchema#unsignedByte":
return "number";
case "http://www.w3.org/2001/XMLSchema#boolean":
return "boolean";
case "http://www.w3.org/2001/XMLSchema#hexBinary":
return "string";
case "http://www.w3.org/2001/XMLSchema#anyURI":
return "iri";
default:
return "string";
}
};
export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
{
@ -8,9 +63,9 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
ShapeDecl: { return: Shape };
Shape: { return: Shape };
EachOf: { return: Shape };
TripleConstraint: { return: SchemaProperty };
NodeConstraint: { return: SchemaValue };
ShapeOr: { return: (SchemaValue | Shape | string)[] };
TripleConstraint: { return: Predicate };
NodeConstraint: { return: DataType };
ShapeOr: { return: (DataType | Shape | string)[] };
ShapeAnd: { return: never };
ShapeNot: { return: never };
ShapeExternal: { return: never };
@ -63,7 +118,7 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
predicates: transformedChildren.expressions.map(
// We disregard cases where properties are referenced (strings)
// or where they consist of Unions or Intersections (not supported).
(expr) => expr as SchemaProperty
(expr) => expr as Predicate
),
};
},
@ -89,9 +144,9 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
// Reference to nested object
return {
type: "nested",
nestedSchema: transformedChildren.valueExpr,
nestedShape: transformedChildren.valueExpr,
...commonProperties,
} satisfies SchemaProperty;
} satisfies Predicate;
} else if (
transformedChildren.valueExpr &&
(transformedChildren.valueExpr as Shape).predicates
@ -99,9 +154,9 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
// Nested object
return {
type: "nested",
nestedSchema: transformedChildren.valueExpr as Shape,
nestedShape: transformedChildren.valueExpr as Shape,
...commonProperties,
} satisfies SchemaProperty;
} satisfies Predicate;
} else if (Array.isArray(transformedChildren.valueExpr)) {
return {
type: "eitherOf",
@ -111,12 +166,12 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
} else {
// type or literal
const nodeConstraint =
transformedChildren.valueExpr as SchemaValue;
transformedChildren.valueExpr as DataType;
return {
type: nodeConstraint.type,
literalValue: nodeConstraint.literals,
...commonProperties,
} satisfies SchemaProperty;
} satisfies Predicate;
}
},
},
@ -124,40 +179,20 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
NodeConstraint: {
transformer: async (nodeConstraint) => {
if (nodeConstraint.datatype) {
switch (nodeConstraint.datatype) {
case "http://www.w3.org/2001/XMLSchema#boolean":
return { type: "boolean" };
case "http://www.w3.org/2001/XMLSchema#byte":
case "http://www.w3.org/2001/XMLSchema#decimal":
case "http://www.w3.org/2001/XMLSchema#double":
case "http://www.w3.org/2001/XMLSchema#float":
case "http://www.w3.org/2001/XMLSchema#int":
case "http://www.w3.org/2001/XMLSchema#integer":
case "http://www.w3.org/2001/XMLSchema#long":
case "http://www.w3.org/2001/XMLSchema#negativeInteger":
case "http://www.w3.org/2001/XMLSchema#nonNegativeInteger":
case "http://www.w3.org/2001/XMLSchema#nonPositiveInteger":
case "http://www.w3.org/2001/XMLSchema#positiveInteger":
case "http://www.w3.org/2001/XMLSchema#short":
case "http://www.w3.org/2001/XMLSchema#unsignedLong":
case "http://www.w3.org/2001/XMLSchema#unsignedInt":
case "http://www.w3.org/2001/XMLSchema#unsignedShort":
case "http://www.w3.org/2001/XMLSchema#unsignedByte":
return { type: "number" };
default:
return { type: "string" }; // treat most as string
}
return {
type: rdfDataTypeToBasic(nodeConstraint.datatype),
};
}
if (nodeConstraint.nodeKind) {
// Something reference-like.
return { type: "string" };
return { type: "iri" };
}
if (nodeConstraint.values) {
return {
type: "literal",
literals: nodeConstraint.values.map(
// TODO: We do not convert them to number or boolean or lang tag.
(valueRecord) => (valueRecord as ObjectLiteral).value
(valueRecord) => valueRecord.value || valueRecord.id
),
};
}
@ -173,12 +208,12 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
// Transformer from ShapeOr
ShapeOr: {
transformer: async (shapeOr, getTransformedChildren) => {
const tc = await getTransformedChildren();
const { shapeExprs } = await getTransformedChildren();
// Either a shape IRI, a nested shape or a node CompactSchemaValue (node constraint).
return (Array.isArray(tc) ? tc : [tc]) as (
return (Array.isArray(shapeExprs) ? shapeExprs : [shapeExprs]) as (
| string
| Shape
| SchemaValue
| DataType
)[];
},
},

@ -677,11 +677,8 @@ export const ShexJTypingTransformerCompact = ShexJTraverser.createTransformer<
ShapeOr: {
transformer: async (_shapeOr, getTransformedChildren) => {
const tc = await getTransformedChildren();
const valid: dom.Type[] = [];
tc.shapeExprs.forEach((t) => {
if (typeof t === "object") valid.push(t);
});
return dom.create.union(valid);
return dom.create.union(tc.shapeExprs);
},
},

@ -1,9 +1,9 @@
export interface ShapeType<T extends OrmBase> {
export interface ShapeType<T extends BaseType> {
schema: Schema;
shape: "http://example.org/Cat";
shape: string;
}
export interface OrmBase extends Record<string, any> {
export interface BaseType extends Record<string, any> {
id: string;
}
@ -13,17 +13,17 @@ export type Schema = {
export interface Shape {
iri: string;
predicates: SchemaProperty[];
predicates: Predicate[];
}
export type SchemaValue = {
export type DataType = {
literals?: number[] | string[] | boolean;
type: "number" | "string" | "boolean" | "literal";
type: "number" | "string" | "boolean" | "iri" | "literal";
};
export interface SchemaProperty {
export interface Predicate {
/** Type of property. */
type: "number" | "string" | "boolean" | "literal" | "nested" | "eitherOf";
type: DataType["type"] | "nested" | "eitherOf";
/** The RDF predicate URI. */
predicateUri: string;
/** The alias of the `predicateUri` when serialized to a JSON object. */
@ -31,13 +31,13 @@ export interface SchemaProperty {
/** The required literal value(s), if type is `literal`. Others are allowed, if `extra` is true. */
literalValue?: number | string | boolean | number[] | string[];
/** If type is `nested`, the shape or its IRI. */
nestedSchema?: string | Shape;
nestedShape?: string | Shape;
/** Maximum allowed number of values. `-1` means infinite. */
maxCardinality: number;
/** Minimum required number of values */
minCardinality: number;
/** If type is `eitherOf`, specifies multiple allowed types (CompactSchemaValue, shapes, or shape IRI). */
eitherOf?: (SchemaValue | Shape | string)[];
eitherOf?: (DataType | Shape | string)[];
/** If other (additional) values are permitted. Useful for literals. */
extra?: boolean;
}

@ -10,9 +10,9 @@ import type {
DeepPatch,
DeepSignalObject,
} from "@nextgraph-monorepo/ng-alien-deepsignals";
import type { ShapeType, OrmBase } from "@nextgraph-monorepo/ng-shex-orm";
import type { ShapeType, BaseType } from "@nextgraph-monorepo/ng-shex-orm";
interface PoolEntry<T extends OrmBase> {
interface PoolEntry<T extends BaseType> {
connectionId: string;
key: string;
shapeType: ShapeType<T>;
@ -37,7 +37,7 @@ interface WasmMessage {
connectionId: string;
diff?: Diff;
shapeType?: ShapeType<any>;
initialData?: OrmBase;
initialData?: BaseType;
}
function canonicalScope(scope: Scope | undefined): string {
@ -150,7 +150,7 @@ const cleanupSignalRegistry =
})
: null;
export function createSignalObjectForShape<T extends OrmBase>(
export function createSignalObjectForShape<T extends BaseType>(
shapeType: ShapeType<T>,
scope?: Scope
) {

@ -1,11 +1,11 @@
import type { OrmBase } from "@nextgraph-monorepo/ng-shex-orm";
import type { BaseType } from "@nextgraph-monorepo/ng-shex-orm";
import { watch } from "@nextgraph-monorepo/ng-alien-deepsignals";
import type { ShapeType } from "@nextgraph-monorepo/ng-shex-orm";
import { useEffect, useRef, useState } from "react";
import { createSignalObjectForShape } from "../../connector/createSignalObjectForShape.ts";
import type { Scope } from "../../types.ts";
const useShape = <T extends OrmBase>(
const useShape = <T extends BaseType>(
shape: ShapeType<T>,
scope: Scope = ""
) => {

@ -7,7 +7,7 @@ import {
getDeepSignalRootId,
type DeepPatch,
} from "@nextgraph-monorepo/ng-alien-deepsignals";
import type { OrmBase, ShapeType } from "@nextgraph-monorepo/ng-shex-orm";
import type { BaseType, ShapeType } from "@nextgraph-monorepo/ng-shex-orm";
/** Base result contract for a deepSignal-backed Svelte integration. */
export interface UseDeepSignalResult<T = any> extends Readable<T> {
@ -88,7 +88,7 @@ export interface UseShapeRuneResult<T = any> extends UseDeepSignalResult<T> {
/**
* Shape-specific rune: constructs the signal object for a shape then delegates to {@link useDeepSignal}.
*/
export function useShapeRune<T extends OrmBase>(
export function useShapeRune<T extends BaseType>(
shape: ShapeType<T>,
scope?: Scope
): UseShapeRuneResult<T | {}> {

@ -2,9 +2,9 @@ import { createSignalObjectForShape } from "../../connector/createSignalObjectFo
import type { Scope } from "../../types.ts";
import useDeepSignal from "./useDeepSignal.ts";
import { onBeforeUnmount } from "vue";
import type { OrmBase, ShapeType } from "@nextgraph-monorepo/ng-shex-orm";
import type { BaseType, ShapeType } from "@nextgraph-monorepo/ng-shex-orm";
export function useShape<T extends OrmBase>(
export function useShape<T extends BaseType>(
shape: ShapeType<T>,
scope?: Scope
) {

Loading…
Cancel
Save