Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
19 changes: 19 additions & 0 deletions connection/src/connector-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,32 @@ import {
} from "./generalized/connector-plugin";
import { neo4jPlugin } from "./neo4j/plugin";
import { postgresPlugin } from "./postgresql/plugin";
import { EXTERNAL_CONNECTORS } from "./external-connectors.generated";

const registry: ConnectorRegistry = createConnectorRegistry();

// Register built-in connectors
registry.register(neo4jPlugin);
registry.register(postgresPlugin);

// ── External connectors (from neoboard-connectors.json) ─────────────────
// Registered AFTER built-ins. Same-type duplicates without overrides throw
// loudly so operators spot the conflict at startup.
for (const { plugin, overrides } of EXTERNAL_CONNECTORS) {
if (registry.has(plugin.type)) {
if (!overrides) {
throw new Error(
'External connector "' +
plugin.type +
'" conflicts with an existing connector. ' +
'Set "overrides": true in neoboard-connectors.json to replace the built-in.',
);
}
registry.unregister(plugin.type);
}
registry.register(plugin);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Re-export for external use
export { registry as connectorRegistry };
export type { ConnectorPlugin, ConnectorRegistry };
Expand Down
13 changes: 13 additions & 0 deletions connection/src/external-connectors.generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* AUTO-GENERATED — do not edit by hand.
* Source: neoboard-connectors.json
* Regenerate: node scripts/generate-connector-imports.mjs
*/
import type { ConnectorPlugin } from "./generalized/connector-plugin";

export interface ExternalConnectorEntry {
plugin: ConnectorPlugin;
overrides: boolean;
}

export const EXTERNAL_CONNECTORS: ExternalConnectorEntry[] = [];
3 changes: 3 additions & 0 deletions neoboard-connectors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"connectors": []
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
],
"scripts": {
"generate:plugins": "node scripts/generate-plugin-imports.mjs",
"predev": "node scripts/generate-plugin-imports.mjs",
"prebuild": "node scripts/generate-plugin-imports.mjs",
"generate:connectors": "node scripts/generate-connector-imports.mjs",
"predev": "node scripts/generate-plugin-imports.mjs && node scripts/generate-connector-imports.mjs",
"prebuild": "node scripts/generate-plugin-imports.mjs && node scripts/generate-connector-imports.mjs",
"dev": "npm -w app run dev",
"build": "npm -w connection run build && npm -w app run build",
"test": "npm -w app run test && npm -w component run test && npm -w cli run test",
Expand Down
128 changes: 128 additions & 0 deletions scripts/__tests__/generate-connector-imports.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { describe, it, expect } from "vitest";
import {
validateEntry,
validateManifest,
renderSource,
} from "../generate-connector-imports.mjs";

describe("validateEntry", () => {
it("accepts a valid entry with package only", () => {
expect(validateEntry({ package: "@myorg/neoboard-mongodb" }, 0)).toBeNull();
});

it("accepts a valid entry with all fields", () => {
expect(
validateEntry(
{ package: "@myorg/neoboard-mongodb", export: "plugin", overrides: true },
0,
),
).toBeNull();
});

it("rejects non-object", () => {
expect(validateEntry("string", 0)).toContain("must be an object");
});

it("rejects missing package", () => {
expect(validateEntry({}, 0)).toContain("package must be a non-empty string");
});

it("rejects empty package", () => {
expect(validateEntry({ package: "" }, 0)).toContain("non-empty");
});

it("rejects package with spaces", () => {
expect(validateEntry({ package: "my package" }, 0)).toContain(
"whitespace",
);
});

it("rejects invalid export identifier", () => {
expect(
validateEntry({ package: "pkg", export: "not-valid" }, 0),
).toContain("valid JavaScript identifier");
});

it("accepts 'default' export", () => {
expect(
validateEntry({ package: "pkg", export: "default" }, 0),
).toBeNull();
});

it("rejects unknown keys", () => {
expect(
validateEntry({ package: "pkg", extra: true }, 0),
).toContain('unknown key "extra"');
});
});

describe("validateManifest", () => {
it("accepts empty connectors array", () => {
const { errors, entries } = validateManifest({ connectors: [] });
expect(errors).toHaveLength(0);
expect(entries).toHaveLength(0);
});

it("accepts valid entries", () => {
const { errors, entries } = validateManifest({
connectors: [{ package: "@myorg/mongodb" }],
});
expect(errors).toHaveLength(0);
expect(entries).toHaveLength(1);
expect(entries[0].package).toBe("@myorg/mongodb");
expect(entries[0].export).toBe("default");
expect(entries[0].overrides).toBe(false);
});

it("rejects non-object manifest", () => {
const { errors } = validateManifest("bad");
expect(errors[0]).toContain("must be a JSON object");
});

it("rejects missing connectors key", () => {
const { errors } = validateManifest({});
expect(errors[0]).toContain("must be an array");
});

it("detects duplicate entries", () => {
const { errors } = validateManifest({
connectors: [
{ package: "@myorg/mongodb" },
{ package: "@myorg/mongodb" },
],
});
expect(errors).toHaveLength(1);
expect(errors[0]).toContain("duplicate");
});
});

describe("renderSource", () => {
it("renders empty array for no entries", () => {
const source = renderSource([]);
expect(source).toContain("EXTERNAL_CONNECTORS: ExternalConnectorEntry[] = []");
});

it("renders import for default export", () => {
const source = renderSource([
{ package: "@myorg/mongodb", export: "default", overrides: false },
]);
expect(source).toContain('import externalConnector0 from "@myorg/mongodb"');
expect(source).toContain("plugin: externalConnector0");
expect(source).toContain("overrides: false");
});

it("renders import for named export", () => {
const source = renderSource([
{ package: "@myorg/mongodb", export: "plugin", overrides: true },
]);
expect(source).toContain(
'{ plugin as externalConnector0 } from "@myorg/mongodb"',
);
expect(source).toContain("overrides: true");
});

it("renders ConnectorPlugin type reference", () => {
const source = renderSource([]);
expect(source).toContain("ConnectorPlugin");
});
});
Loading
Loading