mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-22 03:52:48 +05:30
feat: Added repository abstraction for worldState and Entity (closes #4)
This commit is contained in:
@@ -5,7 +5,7 @@ import tseslint from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ["**/dist/**", "**/node_modules/**"],
|
||||
ignores: ["**/dist/**", "**/node_modules/**", "content/scenario-builder/**"],
|
||||
},
|
||||
|
||||
js.configs.recommended,
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"eslint": "^10.6.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
|
||||
@@ -5,5 +5,8 @@
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^12.11.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,10 @@ export class Attribute {
|
||||
this.allowedEntities.delete(objectId);
|
||||
}
|
||||
}
|
||||
|
||||
getAllowedEntities(): Set<string> {
|
||||
return new Set(this.allowedEntities);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IAttribute {
|
||||
@@ -73,9 +77,13 @@ export interface IAttribute {
|
||||
}
|
||||
|
||||
export abstract class AttributableObject implements IAttribute {
|
||||
readonly id: string = crypto.randomUUID();
|
||||
readonly id: string;
|
||||
readonly attributes: Map<string, Attribute> = new Map<string, Attribute>();
|
||||
|
||||
constructor(id?: string) {
|
||||
this.id = id ?? crypto.randomUUID();
|
||||
}
|
||||
|
||||
addAttribute(
|
||||
name: string,
|
||||
value: string,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AttributableObject } from "./attribute.js";
|
||||
|
||||
export class Entity extends AttributableObject {
|
||||
constructor() {
|
||||
super();
|
||||
constructor(id?: string) {
|
||||
super(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./attribute.js";
|
||||
export * from "./entity.js";
|
||||
export * from "./world.js";
|
||||
export * from "./repository.js";
|
||||
|
||||
244
packages/core/src/repository.ts
Normal file
244
packages/core/src/repository.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
import { AttributableObject, AttributeVisibility } from "./attribute.js";
|
||||
import { Entity } from "./entity.js";
|
||||
import { WorldState } from "./world.js";
|
||||
|
||||
export class SQLiteRepository {
|
||||
private db: Database.Database;
|
||||
|
||||
constructor(db: Database.Database) {
|
||||
this.db = db;
|
||||
this.initializeSchema();
|
||||
}
|
||||
|
||||
private initializeSchema() {
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS objects (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
world_id TEXT,
|
||||
FOREIGN KEY (world_id) REFERENCES objects(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS attributes (
|
||||
object_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
visibility TEXT NOT NULL,
|
||||
PRIMARY KEY (object_id, name),
|
||||
FOREIGN KEY (object_id) REFERENCES objects(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS attribute_acl (
|
||||
object_id TEXT NOT NULL,
|
||||
attribute_name TEXT NOT NULL,
|
||||
entity_id TEXT NOT NULL,
|
||||
PRIMARY KEY (object_id, attribute_name, entity_id),
|
||||
FOREIGN KEY (object_id, attribute_name) REFERENCES attributes(object_id, name) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
save(obj: AttributableObject, type: string, worldId?: string): void {
|
||||
const saveTx = this.db.transaction(() => {
|
||||
// 1. Insert or ignore the object in the objects table
|
||||
this.db
|
||||
.prepare(
|
||||
`
|
||||
INSERT INTO objects (id, type, world_id)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET type = excluded.type, world_id = excluded.world_id
|
||||
`,
|
||||
)
|
||||
.run(obj.id, type, worldId || null);
|
||||
|
||||
// Get current attributes from db to delete the ones that are no longer present
|
||||
const existingAttrs = this.db
|
||||
.prepare(
|
||||
`
|
||||
SELECT name FROM attributes WHERE object_id = ?
|
||||
`,
|
||||
)
|
||||
.all(obj.id) as { name: string }[];
|
||||
|
||||
const existingNames = new Set(existingAttrs.map((a) => a.name));
|
||||
const currentNames = new Set(obj.attributes.keys());
|
||||
|
||||
// Delete attributes that are no longer on the object
|
||||
for (const name of existingNames) {
|
||||
if (!currentNames.has(name)) {
|
||||
this.db
|
||||
.prepare(
|
||||
`
|
||||
DELETE FROM attributes WHERE object_id = ? AND name = ?
|
||||
`,
|
||||
)
|
||||
.run(obj.id, name);
|
||||
}
|
||||
}
|
||||
|
||||
// Save / update current attributes
|
||||
for (const [name, attr] of obj.attributes) {
|
||||
this.db
|
||||
.prepare(
|
||||
`
|
||||
INSERT INTO attributes (object_id, name, value, visibility)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(object_id, name) DO UPDATE SET
|
||||
value = excluded.value,
|
||||
visibility = excluded.visibility
|
||||
`,
|
||||
)
|
||||
.run(obj.id, name, attr.getValue(), attr.getVisibility());
|
||||
|
||||
// Manage ACL
|
||||
// Clear existing ACL entries for this attribute
|
||||
this.db
|
||||
.prepare(
|
||||
`
|
||||
DELETE FROM attribute_acl WHERE object_id = ? AND attribute_name = ?
|
||||
`,
|
||||
)
|
||||
.run(obj.id, name);
|
||||
|
||||
// Insert new ACL entries
|
||||
if (attr.getVisibility() === AttributeVisibility.PRIVATE) {
|
||||
const allowed = attr.getAllowedEntities();
|
||||
const insertAcl = this.db.prepare(`
|
||||
INSERT INTO attribute_acl (object_id, attribute_name, entity_id)
|
||||
VALUES (?, ?, ?)
|
||||
`);
|
||||
for (const entityId of allowed) {
|
||||
insertAcl.run(obj.id, name, entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
saveTx();
|
||||
}
|
||||
|
||||
saveEntity(entity: Entity, worldId?: string): void {
|
||||
this.save(entity, "entity", worldId);
|
||||
}
|
||||
|
||||
saveWorldState(worldState: WorldState): void {
|
||||
const saveWorldTx = this.db.transaction(() => {
|
||||
this.save(worldState, "world");
|
||||
for (const entity of worldState.entities.values()) {
|
||||
this.saveEntity(entity, worldState.id);
|
||||
}
|
||||
});
|
||||
saveWorldTx();
|
||||
}
|
||||
|
||||
loadEntity(id: string): Entity | null {
|
||||
const objRow = this.db
|
||||
.prepare(
|
||||
`
|
||||
SELECT type FROM objects WHERE id = ?
|
||||
`,
|
||||
)
|
||||
.get(id) as { type: string } | undefined;
|
||||
|
||||
if (!objRow || objRow.type !== "entity") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entity = new Entity(id);
|
||||
this.reconstituteAttributes(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
loadWorldState(id: string): WorldState | null {
|
||||
const objRow = this.db
|
||||
.prepare(
|
||||
`
|
||||
SELECT type FROM objects WHERE id = ?
|
||||
`,
|
||||
)
|
||||
.get(id) as { type: string } | undefined;
|
||||
|
||||
if (!objRow || objRow.type !== "world") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const worldState = new WorldState(id);
|
||||
this.reconstituteAttributes(worldState);
|
||||
|
||||
// Reconstitute all entities belonging to this world
|
||||
const entityRows = this.db
|
||||
.prepare(
|
||||
`
|
||||
SELECT id FROM objects WHERE type = 'entity' AND world_id = ?
|
||||
`,
|
||||
)
|
||||
.all(id) as { id: string }[];
|
||||
|
||||
for (const row of entityRows) {
|
||||
const entity = new Entity(row.id);
|
||||
this.reconstituteAttributes(entity);
|
||||
worldState.addEntity(entity);
|
||||
}
|
||||
|
||||
return worldState;
|
||||
}
|
||||
|
||||
listEntities(): Entity[] {
|
||||
const rows = this.db
|
||||
.prepare(
|
||||
`
|
||||
SELECT id FROM objects WHERE type = 'entity'
|
||||
`,
|
||||
)
|
||||
.all() as { id: string }[];
|
||||
|
||||
const entities: Entity[] = [];
|
||||
for (const row of rows) {
|
||||
const entity = new Entity(row.id);
|
||||
this.reconstituteAttributes(entity);
|
||||
entities.push(entity);
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
delete(id: string): void {
|
||||
this.db.prepare(`DELETE FROM objects WHERE id = ?`).run(id);
|
||||
}
|
||||
|
||||
private reconstituteAttributes(obj: AttributableObject): void {
|
||||
// Clear the auto-generated empty maps/attributes if any
|
||||
obj.attributes.clear();
|
||||
|
||||
const attrs = this.db
|
||||
.prepare(
|
||||
`
|
||||
SELECT name, value, visibility FROM attributes WHERE object_id = ?
|
||||
`,
|
||||
)
|
||||
.all(obj.id) as {
|
||||
name: string;
|
||||
value: string;
|
||||
visibility: AttributeVisibility;
|
||||
}[];
|
||||
|
||||
for (const attrRow of attrs) {
|
||||
const allowedRows = this.db
|
||||
.prepare(
|
||||
`
|
||||
SELECT entity_id FROM attribute_acl WHERE object_id = ? AND attribute_name = ?
|
||||
`,
|
||||
)
|
||||
.all(obj.id, attrRow.name) as { entity_id: string }[];
|
||||
|
||||
const allowedEntities = new Set(allowedRows.map((r) => r.entity_id));
|
||||
obj.addAttribute(
|
||||
attrRow.name,
|
||||
attrRow.value,
|
||||
attrRow.visibility,
|
||||
allowedEntities,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,27 @@
|
||||
import { AttributableObject } from "./attribute.js";
|
||||
import { Entity } from "./entity.js";
|
||||
|
||||
export class World extends AttributableObject {
|
||||
constructor() {
|
||||
super();
|
||||
export class WorldState extends AttributableObject {
|
||||
/**
|
||||
* WorldState is the live, evolving instance you get from loading a Scenario and playing it forward.
|
||||
* Universe's current state (distinct from how it started)
|
||||
*/
|
||||
readonly entities: Map<string, Entity> = new Map();
|
||||
|
||||
constructor(id?: string) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
addEntity(entity: Entity): void {
|
||||
if (this.entities.has(entity.id)) {
|
||||
throw new Error(
|
||||
`Entity with ID ${entity.id} already exists in the world`,
|
||||
);
|
||||
}
|
||||
this.entities.set(entity.id, entity);
|
||||
}
|
||||
|
||||
getEntity(id: string): Entity | undefined {
|
||||
return this.entities.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
3495
pnpm-lock.yaml
generated
3495
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,5 +2,8 @@ packages:
|
||||
- "packages/*"
|
||||
- "cli"
|
||||
- "web/*"
|
||||
- "content/scenario-builder"
|
||||
allowBuilds:
|
||||
better-sqlite3: true
|
||||
sharp: true
|
||||
unrs-resolver: true
|
||||
|
||||
Reference in New Issue
Block a user