Skip to content

Commit

Permalink
feat: add signal (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
morten-olsen authored Feb 18, 2025
1 parent a474aec commit edee780
Show file tree
Hide file tree
Showing 29 changed files with 2,346 additions and 7 deletions.
10 changes: 7 additions & 3 deletions demo/backend/nexus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,26 @@ import { linear } from '@bitlerjs/nexus-linear';
import { homeassistant } from '@bitlerjs/nexus-homeassistant';
import { notes } from '@bitlerjs/nexus-notes';
import { timers } from '@bitlerjs/nexus-timers';
import { signal } from '@bitlerjs/nexus-signal';
import { notifications } from '@bitlerjs/nexus-notifications';

import { todos } from './src/extension.js';

const config = defineConfig({
oidc: process.env.OIDC_ISSUER_URL
? {
issuerUrl: process.env.OIDC_ISSUER_URL,
clientId: process.env.OIDC_CLIENT_ID,
}
issuerUrl: process.env.OIDC_ISSUER_URL,
clientId: process.env.OIDC_CLIENT_ID,
}
: undefined,
extensions: [
defineExtension(todos, {}),
defineExtension(typescript, {}),
defineExtension(llmExtension, {
defaultModel: 'openai.gpt-4o-mini',
}),
defineExtension(signal, {}),
defineExtension(notifications, {}),
defineExtension(linear, {}),
defineExtension(openai, {
kind: 'openai',
Expand Down
2 changes: 2 additions & 0 deletions demo/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"@bitlerjs/nexus-notes": "workspace:*",
"@bitlerjs/nexus-timers": "workspace:*",
"@bitlerjs/nexus-calendars": "workspace:*",
"@bitlerjs/nexus-signal": "workspace:*",
"@bitlerjs/nexus-notifications": "workspace:*",
"dotenv": "^16.4.7"
}
}
2 changes: 2 additions & 0 deletions extensions/nexus-notifications/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/dist/
/node_modules/
22 changes: 22 additions & 0 deletions extensions/nexus-notifications/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@bitlerjs/nexus-notifications",
"version": "1.0.0",
"main": "dist/exports.js",
"type": "module",
"files": [
"dist"
],
"scripts": {
"build": "tsc --build",
"dev": "tsc --build --watch"
},
"devDependencies": {
"@bitlerjs/nexus-config": "workspace:*",
"typescript": "^5.7.2"
},
"dependencies": {
"@bitlerjs/nexus": "workspace:*",
"@bitlerjs/nexus-data": "workspace:*",
"nanoid": "^5.0.9"
}
}
43 changes: 43 additions & 0 deletions extensions/nexus-notifications/src/databases/databases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createDatabase, createMigration } from '@bitlerjs/nexus-data';

const init = createMigration({
name: 'init',
up: async (knex) => {
await knex.schema.createTable('notifications', (table) => {
table.string('id').primary();
table.string('title').notNullable();
table.string('message').notNullable();
table.timestamp('createdAt').defaultTo(knex.fn.now());
});

await knex.schema.createTable('notificationActions', (table) => {
table.string('id').primary();
table.string('title').notNullable();
table.string('description').nullable();
table.string('notificationId').notNullable().references('id').inTable('notifications').onDelete('CASCADE');
table.string('kind').notNullable();
table.boolean('removeNotification').defaultTo(false);
table.json('data').notNullable();
});

await knex.schema.createTable('notificationEntities', (table) => {
table.string('notificationId').notNullable().references('id').inTable('notifications').onDelete('CASCADE');
table.string('kind').notNullable();
table.string('role').nullable();
table.string('entityId').notNullable();
table.primary(['notificationId', 'kind', 'entityId']);
});
},
down: async (knex) => {
await knex.schema.dropTable('notificationEntities');
await knex.schema.dropTable('notificationActions');
await knex.schema.dropTable('notifications');
},
});

const dbConfig = createDatabase({
name: 'notifications',
migrations: [init],
});

export { dbConfig };
29 changes: 29 additions & 0 deletions extensions/nexus-notifications/src/events/events.created.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createEvent, Type } from '@bitlerjs/nexus';

const notificationCreatedEvent = createEvent({
kind: 'notification.created',
name: 'Notification created',
description: 'A notification was created',
input: Type.Object({}),
output: Type.Object({
id: Type.String(),
title: Type.String(),
message: Type.String(),
entities: Type.Array(
Type.Object({
kind: Type.String(),
id: Type.String(),
role: Type.Optional(Type.String()),
}),
),
actions: Type.Array(
Type.Object({
id: Type.String(),
title: Type.String(),
description: Type.Optional(Type.String()),
}),
),
}),
});

export { notificationCreatedEvent };
18 changes: 18 additions & 0 deletions extensions/nexus-notifications/src/events/events.removed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createEvent, Type } from '@bitlerjs/nexus';

const notificationRemovedEvent = createEvent({
kind: 'notification.removed',
name: 'Notification Removed',
description: 'A notification was removed',
input: Type.Object({
ids: Type.Optional(Type.Array(Type.String())),
}),
output: Type.Object({
id: Type.String(),
}),
filter: async ({ input, event }) => {
return input.ids?.includes(event.id) || false;
},
});

export { notificationRemovedEvent };
28 changes: 28 additions & 0 deletions extensions/nexus-notifications/src/exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createExtension, EventsService, TasksService } from '@bitlerjs/nexus';

import { notificationRemovedEvent } from './events/events.removed.js';
import { notificationCreatedEvent } from './events/events.created.js';
import { addNotificationTask } from './tasks/tasks.add.js';
import { listNotificationTask } from './tasks/tasks.list.js';
import { removeNotificationsTask } from './tasks/tasks.remove.js';
import { runNotificationActionTask } from './tasks/tasks.run-action.js';

const tasks = {
addNotification: addNotificationTask,
listNotifications: listNotificationTask,
removeNotifications: removeNotificationsTask,
runNotificationAction: runNotificationActionTask,
};

const events = [notificationRemovedEvent, notificationCreatedEvent];
const notifications = createExtension({
setup: async ({ container }) => {
const tasksService = container.get(TasksService);
tasksService.register(Object.values(tasks));

const eventsService = container.get(EventsService);
eventsService.register(Object.values(events));
},
});

export { notifications, tasks, events };
88 changes: 88 additions & 0 deletions extensions/nexus-notifications/src/tasks/tasks.add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { EventsService, Type, createTask } from '@bitlerjs/nexus';
import { nanoid } from 'nanoid';
import { Databases } from '@bitlerjs/nexus-data';

import { dbConfig } from '../databases/databases.js';
import { notificationCreatedEvent } from '../events/events.created.js';

const addNotificationTask = createTask({
kind: 'notification.add',
name: 'Add notification',
description: 'Add a notification',
input: Type.Object({
id: Type.Optional(Type.String()),
title: Type.String(),
message: Type.String(),
entities: Type.Optional(
Type.Array(
Type.Object({
kind: Type.String(),
id: Type.String(),
rolw: Type.Optional(Type.String()),
}),
),
),
actions: Type.Optional(
Type.Array(
Type.Object({
title: Type.String(),
description: Type.Optional(Type.String()),
kind: Type.String(),
removeNotification: Type.Optional(Type.Boolean()),
data: Type.Unknown(),
}),
),
),
}),
output: Type.Object({
id: Type.String(),
}),
handler: async ({ input, container }) => {
const dbs = container.get(Databases);
const eventsService = container.get(EventsService);
const db = await dbs.get(dbConfig);
const id = input.id || nanoid();
const actions = input.actions?.map((action) => ({
id,
title: action.title,
description: action.description,
kind: action.kind,
removeNotification: action.removeNotification || false,
data: action.data,
}));

await db.transaction(async (trx) => {
await trx('notifications')
.insert({
id,
title: input.title,
message: input.message,
})
.onConflict('id')
.merge();

await trx('notificationActions').where('notificationId', id).delete();
await trx('notificationEntities').where('notificationId', id).delete();

if (actions) {
await trx('notificationActions').insert(actions);
}

if (input.entities) {
await trx('notificationEntities').insert(input.entities.map((entity) => ({ ...entity, notificationId: id })));
}
});

eventsService.emit(notificationCreatedEvent, {
id,
title: input.title,
message: input.message,
actions: actions || [],
entities: input.entities || [],
});

return { id };
},
});

export { addNotificationTask };
48 changes: 48 additions & 0 deletions extensions/nexus-notifications/src/tasks/tasks.list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createTask, Type } from '@bitlerjs/nexus';
import { Databases } from '@bitlerjs/nexus-data';

import { dbConfig } from '../databases/databases.js';

const listNotificationTask = createTask({
kind: 'notification.list',
name: 'List',
group: 'Notification',
description: 'List notifications',
input: Type.Object({}),
output: Type.Object({
notifications: Type.Array(
Type.Object({
id: Type.String(),
title: Type.String(),
message: Type.String(),
createdAt: Type.String(),
actions: Type.Array(
Type.Object({
id: Type.String(),
title: Type.String(),
description: Type.Optional(Type.String()),
kind: Type.String(),
removeNotification: Type.Optional(Type.Boolean()),
data: Type.Unknown(),
}),
),
}),
),
}),
handler: async ({ container }) => {
const dbs = container.get(Databases);
const db = await dbs.get(dbConfig);

const notifications = await db('notifications').select('*');
const actions = await db('notificationActions').select('*');

const result = notifications.map((notification) => ({
...notification,
actions: actions.filter((action) => action.notificationId === notification.id),
}));

return { notifications: result };
},
});

export { listNotificationTask };
35 changes: 35 additions & 0 deletions extensions/nexus-notifications/src/tasks/tasks.remove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createTask, EventsService, Type } from '@bitlerjs/nexus';
import { Databases } from '@bitlerjs/nexus-data';

import { dbConfig } from '../databases/databases.js';
import { notificationRemovedEvent } from '../events/events.removed.js';

const removeNotificationsTask = createTask({
kind: 'notification.remove',
name: 'Remove',
group: 'Notification',
description: 'Remove a notification',
input: Type.Object({
ids: Type.Array(Type.String()),
}),
output: Type.Object({
success: Type.Boolean(),
}),
handler: async ({ input, container }) => {
const dbs = container.get(Databases);
const eventsService = container.get(EventsService);
const db = await dbs.get(dbConfig);

await db.transaction(async (trx) => {
await trx('notifications').whereIn('id', input.ids).delete();
});

input.ids.forEach((id) => {
eventsService.emit(notificationRemovedEvent, { id });
});

return { success: true };
},
});

export { removeNotificationsTask };
Loading

0 comments on commit edee780

Please sign in to comment.