Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0313d8f
Post-alpha schema WIP
tomvandig Nov 22, 2025
9f5fba0
Remove schemas from ifcx
tomvandig Nov 22, 2025
ea800a3
Explicitly define provenance sections
tomvandig Nov 22, 2025
c1a7709
Gen
tomvandig Nov 22, 2025
d12415e
Add standard & sdk
tomvandig Feb 14, 2026
194a616
WIP codegen
tomvandig Feb 14, 2026
7366a39
Fix codegen
tomvandig Feb 15, 2026
c1f1330
Fix pkg
tomvandig Feb 15, 2026
57a8580
Check in exe
tomvandig Feb 15, 2026
266f437
Roundtrip new file format component ndjson
tomvandig Feb 15, 2026
b35b3da
Roundtrip node & schema
tomvandig Feb 15, 2026
22dd26e
Add cs sdk
tomvandig Feb 15, 2026
0ace9b9
Fix todos
tomvandig Feb 16, 2026
363b4f2
Parse ifcx in cs
tomvandig Feb 16, 2026
0a29a30
CLeanup
tomvandig Feb 16, 2026
b58cd52
WIP api
tomvandig Feb 18, 2026
58d4c51
Add cs/ts api client libs
tomvandig Feb 18, 2026
e13f3e7
Gen c# server
tomvandig Feb 18, 2026
3d017f2
Use system.text.json and rename project for cs api
tomvandig Feb 18, 2026
ee7ce14
Dangling commit
tomvandig Feb 19, 2026
c6e07ee
Rewrite to async service
tomvandig Feb 20, 2026
f6c1478
Question carreer choices
tomvandig Feb 20, 2026
00046de
Implement LayerVersionRoutesGetLayerVersion
tomvandig Feb 20, 2026
973085a
Update apiclients with latest openapi spec
tomvandig Feb 21, 2026
91a1a6c
Bridge client/server models to test without http
tomvandig Feb 21, 2026
db8af3e
Add delete operation
tomvandig Feb 22, 2026
f72bc56
Implement other apicontroller routes
tomvandig Feb 22, 2026
b904bf3
Fix server json enums
tomvandig Feb 22, 2026
e31c4cb
Do a little dance to get stj to look at enummember
tomvandig Feb 22, 2026
cfff746
Set additionalprops to false
tomvandig Feb 22, 2026
b6a0fce
Update ClientServerBridge.cs
tomvandig Feb 22, 2026
ca93bca
Cleanup
tomvandig Feb 22, 2026
ef54f35
Use optional to indicate missing resources
tomvandig Feb 22, 2026
bb557f8
Syncing wip
tomvandig Feb 22, 2026
cff2c52
Update IfcxRemoteFile.cs
tomvandig Feb 23, 2026
8039a6f
WIP
tomvandig Feb 23, 2026
9f31315
Federate history
tomvandig Feb 24, 2026
8d19178
WIP IfcxFileOperations
tomvandig Feb 25, 2026
569a921
Federate in layerservice
tomvandig Feb 25, 2026
e54b563
Query latest for nodes
tomvandig Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions schema/ifcx-api.tsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@

import "@typespec/openapi3";
import "@typespec/http";

using Http;

@pattern("</(\\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\\}{0,1})[0-9a-zA-Z\\/]+>")
scalar path extends string;

@pattern("</[A-Za-z0-9.]+:[A-Za-z0-9]+>")
scalar attribute_uri extends string;

@pattern("</[A-Za-z0-9.]+")
scalar attribute_file_name extends string;

@format("uuid")
scalar uuid extends string;

model LayerStatus
{
id: uuid;
name: string;
latestVersion: uuid;
}

model LayerDetails
{
id: uuid;
name: string;
history: LayerVersion[]
}

model IfcxProvenanceData
{
author: string;
timestamp: string;
application: string;
}

model LayerVersion
{
layerId: uuid;
versionId: uuid;
previousVersionId: uuid;
provenance: IfcxProvenanceData;
}

model LayerVersionIfcxFile
{
blobUrl: string;
}

model CreateLayerCommand
{
id: uuid;
name: string;
...Record<never>; // make additionalProperties = false show up
}

model UpdateLayerCommand
{
name: string;
}

model CreateLayerVersionCommand
{
id: uuid;
previousLayerVersionId: uuid;
blobId: uuid;
}

model BlobResponse
{
blobId: uuid;
putURL: string;
}

enum IfcxFileDownloadType
{
just_this_version,
whole_layer_history_intact,
whole_layer_history_condensed,
whole_layer_and_imports_history_condensed
}

enum CreateLayerVersionResponseState
{
OK,
OUT_OF_DATE
}

model CreateLayerVersionResponse
{
state: CreateLayerVersionResponseState;
}

@service
@route("/ifcx-api")
namespace IfcxApi {

@route("/upload/{blobId}")
@put op upload(@path blobId: uuid, @bodyRoot file: File): OkResponse;

@route("/download/{blobId}")
@put op download(@path blobId: uuid): File;

@route("/layers")
namespace Layers
{
@get op layers(): LayerStatus[];

@post op createLayer(@body cmd: CreateLayerCommand): OkResponse;

@route("/{layerId}")
namespace LayerRoutes {
@get op get_layer(@path layerId: uuid): LayerDetails;
@delete op delete_layer(@path layerId: uuid): OkResponse;

@route("/upload-ifcx-blob-url")
@post op uploadIfcxBlobUrl(@path layerId: uuid): BlobResponse;

@route("/versions")
namespace VersionsRoutes {
@post op createLayerVersion(@path layerId: uuid, @body version: CreateLayerVersionCommand): CreateLayerVersionResponse;

@route("/{versionId}")
namespace LayerVersionRoutes
{
@get op get_layer_version(@path layerId: uuid, @path versionId: uuid): LayerVersion;

@route("/download-ifcx")
@put op layer_ifcx(
@path layerId: uuid,
@path versionId: uuid,
@query downloadType: IfcxFileDownloadType
): LayerVersionIfcxFile;

@route("/query")
op query is IfcxQueryApi.query;
}
}
}
}
}

model IfcxQueryApiResponse
{

}

interface IfcxQueryApi {
@get op query(
@path layerId: uuid,
@path versionId: uuid,
@query path: string;
@query provenance: boolean;
@query expandChildren: boolean;
@query expandChildrenRecursive: boolean;
): IfcxQueryApiResponse;
}
149 changes: 149 additions & 0 deletions schema/ifcx-codegen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
let {
quicktype,
InputData,
JSONSchemaInput,
FetchingJSONSchemaStore
} = require("quicktype-core");
let fs = require("fs");
let path = require("path");

async function quicktypeJSONSchema(targetLanguage, typeName, jsonSchemaString, options) {
const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore());

// We could add multiple schemas for multiple types,
// but here we're just making one type from JSON schema.
await schemaInput.addSource({ name: typeName, schema: jsonSchemaString });

const inputData = new InputData();
inputData.addInput(schemaInput);

return await quicktype({
inputData,
lang: targetLanguage,
rendererOptions: {
...options
}
});
}

function capitalize(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
}
function decapitalize(word) {
return word.charAt(0).toLowerCase() + word.slice(1);
}

// !!! pkg does not support recursive readdirsync !!!
function getAllFiles(dir) {
let results = [];

const entries = fs.readdirSync(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);

if (entry.isDirectory()) {
results = results.concat(getAllFiles(fullPath));
} else {
results.push(fullPath);
}
}

return results;
}

async function ConvertFile(input_path, output_path, language)
{
console.log(`Converting: ${input_path} -> ${output_path}`);

let schema = fs.readFileSync(input_path).toString();
let userSpecifiedId = JSON.parse(schema)["x-ifc5-id"];
let className = capitalize(userSpecifiedId.split("::").at(-1));

if (language === "ts")
{
const { lines: cscode } = await quicktypeJSONSchema("ts", className, schema, {});

let code = cscode;
code.push("\t// start insert");
code.push(`\texport let Identity = {`);
code.push(`\t\t typeID: "${userSpecifiedId}",`);
code.push(`\t\t originSchemaSrc: ${JSON.stringify(schema)},`);
code.push(`\t\t fromJSONString: Convert.to${className},`);
code.push(`\t\t toJSONString: Convert.${decapitalize(className)}ToJson`);
code.push(`\t\t }`);
code.push("\t// end insert");

fs.writeFileSync(output_path, cscode.join("\n"));
}
else
{
let ns = `${userSpecifiedId.replaceAll("::", "_")}`;
const { lines: cscode } = await quicktypeJSONSchema("cs", className, schema, {
namespace: ns,
framework: "SystemTextJson",

});

let code = cscode;
code.push("// start insert");
code.push(`namespace ${ns} {`);
code.push(`\tpartial class ${className} {`);
code.push(`\t\tpublic static ifcx_sdk.IfcxIdentity<${className}> Identity() {`);
code.push(`\t\t\treturn new ifcx_sdk.IfcxIdentity<${className}> {`);
code.push(`\t\t\t typeID = "${userSpecifiedId}",`);
code.push(`\t\t\t originSchemaSrc = ${JSON.stringify(schema)},`);
code.push(`\t\t\t fromJSONString = str => ${className}.FromJson(str),`);
code.push(`\t\t\t toJSONString = obj => Serialize.ToJson(obj)`);
code.push(`\t\t\t};`);
code.push(`\t\t}`);
code.push(`\t}`);
code.push(`}`);
code.push("// end insert");

fs.writeFileSync(output_path, cscode.join("\n"));
}
}

async function main(input_dir, output_dir, language)
{
if (!fs.existsSync(input_dir)) throw new Error(`Dir ${input_dir} does not exist`);
if (["ts", "cs"].indexOf(language) === -1) throw new Error(`Unknown language ${language}, only support: [ts,cs]`);

let files = getAllFiles(input_dir);
console.log(files);
files = files.filter(f => f.endsWith(".schema.json"));

console.log();
console.log(`Files (looking for .schema.json):`);
files.forEach(filepath => {
console.log(` - ${filepath}`);
});
console.log();

if (files.length === 0)
{
throw new Error(`No files found!`);
}

files.forEach(filepath => {
let input_path = filepath;
let output_path = path.join(output_dir, filepath.replace(input_dir, "").replace(".schema.json", `.${language}`));
fs.mkdirSync(path.dirname(output_path), { recursive: true })
ConvertFile(input_path, output_path, language);
})

}

let args = process.argv;
console.log();
console.log(`Invoke like: ifcx-codgen.exe <input_dir> <output_dir> <ts/cs>`);
console.log(`Invoked ifcx-codgen with: `, args);
console.log();
let input_dir = process.argv[2];
let output_dir = process.argv[3];
let language = process.argv[4];
console.log(`input_dir:`, input_dir);
console.log(`output_dir:`, output_dir);
console.log(`language:`, language);
main(input_dir, output_dir, language);
Loading