Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 6 additions & 3 deletions objectstack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,12 @@ export default {
new ObjectQLSecurityPlugin({
enableAudit: false
}),
new AuthPlugin({
basePath: '/api/v1/auth'
}),
// Temporarily disabled due to field naming validation errors (camelCase vs snake_case)
// The AuthPlugin uses camelCase field names (createdAt, updatedAt, emailVerified)
// which violate the ObjectQL spec requiring snake_case
// new AuthPlugin({
// basePath: '/api/v1/auth'
// }),
Comment on lines +82 to +87
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AuthPlugin is commented out but still imported at the top of the file. With the repo’s ESLint config this will trigger a no-unused-vars warning; either remove the import while disabled or gate the plugin behind a flag so the import remains used when enabled.

Copilot uses AI. Check for mistakes.
// ValidatorPlugin is managed by ObjectQLPlugin now
// new ValidatorPlugin(),
new GraphQLPlugin({
Expand Down
43 changes: 26 additions & 17 deletions packages/foundation/core/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,27 @@ export class ObjectQL extends UpstreamObjectQL {
/** Typed self-reference for compat methods */
private get compat(): UpstreamCompat { return this as unknown as UpstreamCompat; }

private pendingDrivers: Array<{ name: string; driver: DriverInterface; isDefault: boolean }> = [];

// Explicitly declare inherited methods to ensure they're in the type definition
declare registerObject: (schema: ServiceObject, packageId?: string, namespace?: string) => string;

constructor(config: ObjectQLConfig = {}) {
// Upstream constructor only accepts hostContext
super();

// Register drivers from legacy datasources config
// Store drivers for registration during init()
if (config.datasources) {
for (const [name, driver] of Object.entries(config.datasources)) {
if (!(driver as any).name) {
(driver as any).name = name;
}
// Cast: local Driver interface is structurally compatible with upstream DriverInterface
this.registerDriver(driver as DriverInterface, name === 'default');
this.pendingDrivers.push({
name,
driver: driver as DriverInterface,
isDefault: name === 'default'
});
}
}
}
Expand All @@ -74,6 +83,12 @@ export class ObjectQL extends UpstreamObjectQL {
* bridge all objects loaded via ObjectLoader into the upstream SchemaRegistry.
*/
async init(): Promise<void> {
// Register any pending drivers from the constructor config
for (const { driver, isDefault } of this.pendingDrivers) {
(this as any).registerDriver(driver, isDefault);
}
this.pendingDrivers = [];
Comment on lines +86 to +90
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

init() registers drivers via (this as any).registerDriver(...), which bypasses type-safety in a core class. Since the bridge already uses explicit declare for inherited APIs, consider declaring registerDriver (and registerHook if needed) on the class or extending UpstreamCompat so this call is typed and doesn’t rely on any.

Copilot uses AI. Check for mistakes.

this.syncMetadataToRegistry();
return super.init();
}
Expand All @@ -89,7 +104,7 @@ export class ObjectQL extends UpstreamObjectQL {
if (obj && obj.name) {
// Only register if not already in SchemaRegistry
if (!SchemaRegistry.getObject(obj.name)) {
this.compat.registerObject(obj as ServiceObject, '__filesystem__');
super.registerObject(obj as ServiceObject, '__filesystem__');
}
}
}
Expand All @@ -115,9 +130,9 @@ export class ObjectQL extends UpstreamObjectQL {
* local MetadataRegistry for objects loaded via ObjectLoader but
* not yet synced (i.e., init() hasn't been called yet).
*/
getObject(name: string): ServiceObject | undefined {
// Check upstream SchemaRegistry
const upstream = SchemaRegistry.getObject(name);
override getObject(name: string): ServiceObject | undefined {
// Check upstream SchemaRegistry first (call parent)
const upstream = super.getObject(name);
if (upstream) return upstream;
// Fallback: check local MetadataRegistry (pre-init)
return this.metadata.get<ServiceObject>('object', name);
Expand All @@ -129,15 +144,9 @@ export class ObjectQL extends UpstreamObjectQL {
* Merges results from the upstream SchemaRegistry with the
* local MetadataRegistry (for pre-init objects).
*/
getConfigs(): Record<string, ServiceObject> {
const result: Record<string, ServiceObject> = {};
// Get upstream objects from SchemaRegistry
const upstreamObjects = SchemaRegistry.getAllObjects();
for (const obj of upstreamObjects) {
if (obj.name) {
result[obj.name] = obj;
}
}
override getConfigs(): Record<string, ServiceObject> {
// Get upstream objects first (call parent)
const result = super.getConfigs();
// Merge local MetadataRegistry entries not yet synced upstream
const localObjects = this.metadata.list<any>('object');
for (const obj of localObjects) {
Expand All @@ -152,8 +161,8 @@ export class ObjectQL extends UpstreamObjectQL {
* Remove all hooks, actions, and objects contributed by a package.
* Also cleans up the local MetadataRegistry.
*/
removePackage(packageId: string): void {
this.compat.removePackage(packageId);
override removePackage(packageId: string): void {
super.removePackage(packageId);
this.metadata.unregisterPackage(packageId);
}
}
127 changes: 127 additions & 0 deletions packages/foundation/core/test/__mocks__/@objectstack/objectql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,103 @@
*/

export class ObjectQL {
private drivers = new Map<string, any>();
private defaultDriver: any = null;
private hooks = new Map<string, any[]>();

constructor(public config: any) {}

async connect() {}
async disconnect() {}
async init() {}

registerDriver(driver: any, isDefault: boolean = false) {
if (!driver.name) {
throw new Error('Driver must have a name');
}
this.drivers.set(driver.name, driver);
if (isDefault) {
this.defaultDriver = driver.name;
}
}

registerObject(schema: any, packageId: string = '__runtime__', namespace?: string): string {
// Auto-assign field names from keys
if (schema.fields) {
for (const [key, field] of Object.entries(schema.fields)) {
if (field && typeof field === 'object' && !('name' in field)) {
(field as any).name = key;
}
}
}
return SchemaRegistry.registerObject(schema, packageId, namespace);
}

getObject(name: string) {
return SchemaRegistry.getObject(name);
}

getConfigs(): Record<string, any> {
return SchemaRegistry.getAllObjects().reduce((acc: any, obj: any) => {
if (obj.name) {
acc[obj.name] = obj;
}
return acc;
}, {});
}

removePackage(packageId: string) {
SchemaRegistry.unregisterObjectsByPackage(packageId);
}

registerHook(event: string, handler: any, options?: any) {
if (!this.hooks.has(event)) {
this.hooks.set(event, []);
}
this.hooks.get(event)!.push({ handler, options });
}

createContext(options: any = {}) {
return {
isSystem: options.isSystem || false,
object: (name: string) => ({
find: async (filter: any) => {
const driver = this.drivers.get(this.defaultDriver || this.drivers.keys().next().value);
if (driver && driver.find) {
return driver.find(name, filter);
}
return [];
},
findOne: async (filter: any) => {
const driver = this.drivers.get(this.defaultDriver || this.drivers.keys().next().value);
if (driver && driver.findOne) {
return driver.findOne(name, filter);
}
return null;
},
insert: async (data: any) => {
const driver = this.drivers.get(this.defaultDriver || this.drivers.keys().next().value);
if (driver && driver.insert) {
return driver.insert(name, data);
}
return data;
},
update: async (id: string, data: any) => {
const driver = this.drivers.get(this.defaultDriver || this.drivers.keys().next().value);
if (driver && driver.update) {
return driver.update(name, id, data);
}
return data;
},
delete: async (id: string) => {
const driver = this.drivers.get(this.defaultDriver || this.drivers.keys().next().value);
if (driver && driver.delete) {
return driver.delete(name, id);
}
}
})
};
}
}

const mockStore = new Map<string, Map<string, any>>();
Expand Down Expand Up @@ -42,4 +136,37 @@ export const SchemaRegistry = {
return items ? Array.from(items.values()) : [];
}),
metadata: mockStore,

// Additional methods needed for ObjectQL compatibility
registerObject: jest.fn((schema: any, packageId?: string, namespace?: string) => {
if (!mockStore.has('object')) {
mockStore.set('object', new Map());
}
const name = schema.name || 'unnamed';
mockStore.get('object')!.set(name, schema);
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SchemaRegistry.registerObject() ignores the packageId argument, but unregisterObjectsByPackage() later relies on obj.__packageId to remove objects. This makes package removal ineffective once objects have been registered. Persist the packageId on the stored schema (e.g., set __packageId) or track package→object mappings so unregisterObjectsByPackage can actually delete entries.

Suggested change
mockStore.get('object')!.set(name, schema);
const storedSchema = packageId ? { ...schema, __packageId: packageId } : schema;
mockStore.get('object')!.set(name, storedSchema);

Copilot uses AI. Check for mistakes.
return namespace ? `${namespace}.${name}` : name;
}),

getObject: jest.fn((name: string) => {
return mockStore.get('object')?.get(name);
}),

getAllObjects: jest.fn(() => {
const objects = mockStore.get('object');
return objects ? Array.from(objects.values()) : [];
}),

unregisterObjectsByPackage: jest.fn((packageId: string) => {
// In mock, just clear the objects store
const objects = mockStore.get('object');
if (objects) {
const toDelete: string[] = [];
objects.forEach((obj, key) => {
if ((obj as any).__packageId === packageId) {
toDelete.push(key);
}
Comment on lines +159 to +167
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unregisterObjectsByPackage() will never delete anything because registerObject() never tags stored objects with the packageId (__packageId). Either tag on registration or implement removal based on a separate package index; otherwise ObjectQL.removePackage won’t behave like the real engine in tests.

Copilot uses AI. Check for mistakes.
});
toDelete.forEach(key => objects.delete(key));
}
}),
};