@lobb-js/core 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +48 -0
- package/src/Lobb.ts +150 -0
- package/src/LobbError.ts +105 -0
- package/src/TypesGenerator.ts +11 -0
- package/src/api/WebServer.ts +126 -0
- package/src/api/collections/CollectionControllers.ts +485 -0
- package/src/api/collections/CollectionService.ts +162 -0
- package/src/api/collections/collectionRoutes.ts +105 -0
- package/src/api/collections/collectionStore.ts +647 -0
- package/src/api/collections/transactions.ts +166 -0
- package/src/api/collections/utils.ts +73 -0
- package/src/api/errorHandler.ts +73 -0
- package/src/api/events/index.ts +129 -0
- package/src/api/meta/route.ts +66 -0
- package/src/api/meta/service.ts +163 -0
- package/src/api/middlewares.ts +71 -0
- package/src/api/openApiRoute.ts +1017 -0
- package/src/api/schema/SchemaService.ts +71 -0
- package/src/api/schema/schemaRoutes.ts +13 -0
- package/src/config/ConfigManager.ts +252 -0
- package/src/config/validations.ts +49 -0
- package/src/coreCollections/collectionsCollection.ts +56 -0
- package/src/coreCollections/index.ts +14 -0
- package/src/coreCollections/migrationsCollection.ts +36 -0
- package/src/coreCollections/queryCollection.ts +26 -0
- package/src/coreCollections/workflowsCollection.ts +73 -0
- package/src/coreDbSetup/index.ts +72 -0
- package/src/coreMigrations/index.ts +3 -0
- package/src/database/DatabaseService.ts +44 -0
- package/src/database/DatabaseSyncManager.ts +173 -0
- package/src/database/MigrationsManager.ts +95 -0
- package/src/database/drivers/MongoDriver.ts +750 -0
- package/src/database/drivers/pgDriver/PGDriver.ts +655 -0
- package/src/database/drivers/pgDriver/QueryBuilder.ts +474 -0
- package/src/database/drivers/pgDriver/utils.ts +6 -0
- package/src/events/EventSystem.ts +191 -0
- package/src/events/coreEvents/index.ts +218 -0
- package/src/events/studioEvents/index.ts +32 -0
- package/src/extension/ExtensionSystem.ts +236 -0
- package/src/extension/dashboardRoute.ts +35 -0
- package/src/fields/ArrayField.ts +33 -0
- package/src/fields/BoolField.ts +34 -0
- package/src/fields/DateField.ts +13 -0
- package/src/fields/DateTimeField.ts +13 -0
- package/src/fields/DecimalField.ts +13 -0
- package/src/fields/FieldUtils.ts +56 -0
- package/src/fields/FloatField.ts +13 -0
- package/src/fields/IntegerField.ts +13 -0
- package/src/fields/LongField.ts +13 -0
- package/src/fields/ObjectField.ts +15 -0
- package/src/fields/StringField.ts +13 -0
- package/src/fields/TextField.ts +13 -0
- package/src/fields/TimeField.ts +13 -0
- package/src/index.ts +53 -0
- package/src/studio/Studio.ts +108 -0
- package/src/types/CollectionControllers.ts +15 -0
- package/src/types/DatabaseDriver.ts +115 -0
- package/src/types/Extension.ts +46 -0
- package/src/types/Field.ts +29 -0
- package/src/types/apiSchema.ts +12 -0
- package/src/types/collectionServiceSchema.ts +18 -0
- package/src/types/config/collectionFields.ts +85 -0
- package/src/types/config/collectionsConfig.ts +50 -0
- package/src/types/config/config.ts +66 -0
- package/src/types/config/relations.ts +17 -0
- package/src/types/filterSchema.ts +88 -0
- package/src/types/index.ts +38 -0
- package/src/types/migrations.ts +12 -0
- package/src/types/websockets.ts +34 -0
- package/src/types/workflows/processors.ts +1 -0
- package/src/utils/lockCollectionToObject.ts +204 -0
- package/src/utils/utils.ts +310 -0
- package/src/workflows/WorkflowSystem.ts +182 -0
- package/src/workflows/coreWorkflows/collectionsTable/index.ts +118 -0
- package/src/workflows/coreWorkflows/index.ts +18 -0
- package/src/workflows/coreWorkflows/processors/postOperationsWorkflows.ts +46 -0
- package/src/workflows/coreWorkflows/processors/preOperationsWorkflows.ts +27 -0
- package/src/workflows/coreWorkflows/processors/processorForDB.ts +13 -0
- package/src/workflows/coreWorkflows/processors/processors/processor.ts +23 -0
- package/src/workflows/coreWorkflows/processors/processors/processorsFunctions.ts +47 -0
- package/src/workflows/coreWorkflows/processors/utils.ts +102 -0
- package/src/workflows/coreWorkflows/processors/validator/validator.ts +19 -0
- package/src/workflows/coreWorkflows/processors/validator/validatorsFunction.ts +52 -0
- package/src/workflows/coreWorkflows/queryCoreWorkflows.ts +31 -0
- package/src/workflows/coreWorkflows/utilsCoreWorkflows.ts +40 -0
- package/src/workflows/coreWorkflows/workflowsCollection/workflowsCollectionWorkflows.ts +101 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { CollectionService, TriggeredBy } from "./CollectionService.ts";
|
|
2
|
+
import type { CollectionStore } from "./collectionStore.ts";
|
|
3
|
+
import type { Context as HonoContext } from "hono";
|
|
4
|
+
import type { PoolClient } from "pg";
|
|
5
|
+
import { Lobb } from "../../Lobb.ts";
|
|
6
|
+
import { LobbReturnError } from "../errorHandler.ts";
|
|
7
|
+
|
|
8
|
+
// Helper: add Symbol.dispose to a pg PoolClient so `using` syntax works
|
|
9
|
+
function asDisposable<T extends { release(): void }>(client: T): T & Disposable {
|
|
10
|
+
(client as any)[Symbol.dispose] = () => client.release();
|
|
11
|
+
return client as T & Disposable;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TransactionBody = {
|
|
15
|
+
[
|
|
16
|
+
K in keyof CollectionService as CollectionService[K] extends
|
|
17
|
+
(...args: any[]) => any ? K
|
|
18
|
+
: never
|
|
19
|
+
]: {
|
|
20
|
+
method: K;
|
|
21
|
+
props: K extends keyof CollectionStore
|
|
22
|
+
? Parameters<CollectionStore[K & keyof CollectionStore]>[0]
|
|
23
|
+
: Parameters<CollectionService[K]>[0];
|
|
24
|
+
};
|
|
25
|
+
}[keyof CollectionService];
|
|
26
|
+
|
|
27
|
+
export async function beginTransaction<T>(
|
|
28
|
+
func: (client: PoolClient) => Promise<T>,
|
|
29
|
+
localClient?: PoolClient,
|
|
30
|
+
rollback: boolean = false,
|
|
31
|
+
): Promise<T> {
|
|
32
|
+
if (localClient) {
|
|
33
|
+
return await func(localClient);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const dbDriver = Lobb.instance.databaseService.getDriver();
|
|
37
|
+
const pool = dbDriver.getConnection();
|
|
38
|
+
const client = asDisposable(await pool.connect());
|
|
39
|
+
using _ = client;
|
|
40
|
+
await client.query("BEGIN");
|
|
41
|
+
let returnedValue;
|
|
42
|
+
try {
|
|
43
|
+
returnedValue = await func(client);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
await client.query("ROLLBACK");
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (rollback) {
|
|
50
|
+
await client.query("ROLLBACK");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await client.query("COMMIT");
|
|
54
|
+
|
|
55
|
+
return returnedValue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function transactions({
|
|
59
|
+
body,
|
|
60
|
+
rollback = false,
|
|
61
|
+
triggeredBy,
|
|
62
|
+
context,
|
|
63
|
+
}: {
|
|
64
|
+
body: TransactionBody[];
|
|
65
|
+
rollback?: boolean;
|
|
66
|
+
triggeredBy?: TriggeredBy;
|
|
67
|
+
context?: HonoContext;
|
|
68
|
+
}): Promise<any> {
|
|
69
|
+
// starting the transactions
|
|
70
|
+
const responses = await beginTransaction(
|
|
71
|
+
async (client) => {
|
|
72
|
+
const responses = [];
|
|
73
|
+
for (let index = 0; index < body.length; index++) {
|
|
74
|
+
const transaction = body[index];
|
|
75
|
+
transaction.props = Lobb.instance.utils.renderTemplateDeep(
|
|
76
|
+
transaction.props,
|
|
77
|
+
{
|
|
78
|
+
responses: responses,
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (transaction.method === "findAll") {
|
|
83
|
+
const res = await Lobb.instance.collectionService.findAll({
|
|
84
|
+
...transaction.props,
|
|
85
|
+
context,
|
|
86
|
+
client,
|
|
87
|
+
triggeredBy,
|
|
88
|
+
});
|
|
89
|
+
responses.push(res);
|
|
90
|
+
} else if (transaction.method === "findOne") {
|
|
91
|
+
const res = await Lobb.instance.collectionService.findOne({
|
|
92
|
+
...transaction.props,
|
|
93
|
+
context,
|
|
94
|
+
client,
|
|
95
|
+
triggeredBy,
|
|
96
|
+
});
|
|
97
|
+
responses.push(res);
|
|
98
|
+
} else if (transaction.method === "createOne") {
|
|
99
|
+
const res = await Lobb.instance.collectionService.createOne({
|
|
100
|
+
...transaction.props,
|
|
101
|
+
context,
|
|
102
|
+
client,
|
|
103
|
+
triggeredBy,
|
|
104
|
+
});
|
|
105
|
+
responses.push(res);
|
|
106
|
+
} else if (transaction.method === "updateOne") {
|
|
107
|
+
const res = await Lobb.instance.collectionService.updateOne({
|
|
108
|
+
...transaction.props,
|
|
109
|
+
context,
|
|
110
|
+
client,
|
|
111
|
+
triggeredBy,
|
|
112
|
+
});
|
|
113
|
+
responses.push(res);
|
|
114
|
+
} else if (transaction.method === "deleteOne") {
|
|
115
|
+
const res = await Lobb.instance.collectionService.deleteOne({
|
|
116
|
+
...transaction.props,
|
|
117
|
+
context,
|
|
118
|
+
client,
|
|
119
|
+
triggeredBy,
|
|
120
|
+
});
|
|
121
|
+
responses.push(res);
|
|
122
|
+
} else if (transaction.method === "createMany") {
|
|
123
|
+
const res = await Lobb.instance.collectionService.createMany({
|
|
124
|
+
...transaction.props,
|
|
125
|
+
context,
|
|
126
|
+
client,
|
|
127
|
+
triggeredBy,
|
|
128
|
+
});
|
|
129
|
+
responses.push(res);
|
|
130
|
+
} else if (transaction.method === "updateMany") {
|
|
131
|
+
const res = await Lobb.instance.collectionService.updateMany({
|
|
132
|
+
...transaction.props,
|
|
133
|
+
context,
|
|
134
|
+
client,
|
|
135
|
+
triggeredBy,
|
|
136
|
+
});
|
|
137
|
+
responses.push(res);
|
|
138
|
+
} else if (transaction.method === "deleteMany") {
|
|
139
|
+
const res = await Lobb.instance.collectionService.deleteMany({
|
|
140
|
+
...transaction.props,
|
|
141
|
+
context,
|
|
142
|
+
client,
|
|
143
|
+
triggeredBy,
|
|
144
|
+
});
|
|
145
|
+
responses.push(res);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return responses;
|
|
150
|
+
},
|
|
151
|
+
undefined,
|
|
152
|
+
rollback,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
await Lobb.instance.eventSystem.emit(
|
|
156
|
+
`core.collection.transaction`,
|
|
157
|
+
{
|
|
158
|
+
body: body,
|
|
159
|
+
responses: responses,
|
|
160
|
+
rollback: rollback,
|
|
161
|
+
triggeredBy: triggeredBy,
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return responses;
|
|
166
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Lobb } from "../../Lobb.ts";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export function getCollectionDocumentSchema(
|
|
5
|
+
collectionName?: string,
|
|
6
|
+
) {
|
|
7
|
+
if (collectionName) {
|
|
8
|
+
const collection = Lobb.instance.configManager.getCollection(
|
|
9
|
+
collectionName,
|
|
10
|
+
);
|
|
11
|
+
const fieldNames = Object.keys(collection.fields);
|
|
12
|
+
|
|
13
|
+
const properties: Record<string, any> = {};
|
|
14
|
+
for (let index = 0; index < fieldNames.length; index++) {
|
|
15
|
+
const fieldName = fieldNames[index];
|
|
16
|
+
properties[fieldName] = getColFieldPropSchema(
|
|
17
|
+
collectionName,
|
|
18
|
+
fieldName,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return z.object(properties);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const schema = z.object({
|
|
26
|
+
id: z.number().int().optional(),
|
|
27
|
+
}).passthrough();
|
|
28
|
+
|
|
29
|
+
return schema;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getColFieldPropSchema(
|
|
33
|
+
collectionName: string,
|
|
34
|
+
fieldName: string,
|
|
35
|
+
) {
|
|
36
|
+
const field = Lobb.instance.configManager.getField(
|
|
37
|
+
fieldName,
|
|
38
|
+
collectionName,
|
|
39
|
+
);
|
|
40
|
+
let fieldSchema;
|
|
41
|
+
if (field.type === "bool") {
|
|
42
|
+
fieldSchema = z.boolean();
|
|
43
|
+
} else if (field.type === "date") {
|
|
44
|
+
fieldSchema = z.date();
|
|
45
|
+
} else if (field.type === "time") {
|
|
46
|
+
fieldSchema = z.date();
|
|
47
|
+
} else if (field.type === "datetime") {
|
|
48
|
+
fieldSchema = z.date();
|
|
49
|
+
} else if (field.type === "decimal") {
|
|
50
|
+
fieldSchema = z.string();
|
|
51
|
+
} else if (field.type === "float") {
|
|
52
|
+
fieldSchema = z.number();
|
|
53
|
+
} else if (field.type === "integer") {
|
|
54
|
+
fieldSchema = z.number().int();
|
|
55
|
+
} else if (field.type === "long") {
|
|
56
|
+
fieldSchema = z.bigint();
|
|
57
|
+
} else if (field.type === "string") {
|
|
58
|
+
fieldSchema = z.string().max(field.type.length);
|
|
59
|
+
} else if (field.type === "text") {
|
|
60
|
+
fieldSchema = z.string();
|
|
61
|
+
} else {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`The (${collectionName}.${fieldName}) field has a type thats not implemented in the genColPropsJsonSchema`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const requiredField = Boolean(field.validators?.required);
|
|
68
|
+
if (!requiredField) {
|
|
69
|
+
fieldSchema = fieldSchema.optional();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return fieldSchema;
|
|
73
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { LobbError } from "../LobbError.ts";
|
|
3
|
+
|
|
4
|
+
// pg errors have a 5-char SQLSTATE code and a detail property
|
|
5
|
+
function isPgDatabaseError(error: any): error is { code: string; detail?: string; message: string } {
|
|
6
|
+
return (
|
|
7
|
+
error != null &&
|
|
8
|
+
typeof error.code === "string" &&
|
|
9
|
+
/^[0-9A-Z]{5}$/.test(error.code)
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class LobbReturnError extends Error {
|
|
14
|
+
public any: any;
|
|
15
|
+
|
|
16
|
+
constructor(any: any) {
|
|
17
|
+
super();
|
|
18
|
+
this.any = any;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function errorHandler(error: any, c: Context) {
|
|
23
|
+
if (error instanceof LobbError) {
|
|
24
|
+
return c.json(
|
|
25
|
+
{
|
|
26
|
+
status: error.status,
|
|
27
|
+
code: error.code,
|
|
28
|
+
message: error.message,
|
|
29
|
+
details: error.details,
|
|
30
|
+
},
|
|
31
|
+
error.status as any,
|
|
32
|
+
);
|
|
33
|
+
} else if (isPgDatabaseError(error)) {
|
|
34
|
+
if (error.code === "23505") {
|
|
35
|
+
const match = error.detail?.match(/\((\w+)\)=\(/);
|
|
36
|
+
if (match) {
|
|
37
|
+
const uniqeFieldName = match[1];
|
|
38
|
+
throw new LobbError({
|
|
39
|
+
code: "CONFLICT",
|
|
40
|
+
message: `Violation of the uniqueness constraint.`,
|
|
41
|
+
details: {
|
|
42
|
+
[uniqeFieldName]: [
|
|
43
|
+
`Duplicate value found, this field must be unique.`,
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
throw new LobbError({
|
|
49
|
+
code: "CONFLICT",
|
|
50
|
+
message: `One or more fields violate the uniqueness constraint.`,
|
|
51
|
+
details: error.detail,
|
|
52
|
+
});
|
|
53
|
+
} else if (error.code === "22P02") {
|
|
54
|
+
throw new LobbError({
|
|
55
|
+
code: "BAD_REQUEST",
|
|
56
|
+
message: error.message,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
} else if (error instanceof LobbReturnError) {
|
|
60
|
+
return error.any;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// TODO: replace this with a better logging system
|
|
64
|
+
console.error(error);
|
|
65
|
+
return c.json(
|
|
66
|
+
{
|
|
67
|
+
status: 500,
|
|
68
|
+
code: "INTERNAL_ERROR",
|
|
69
|
+
message: "An unexpected error occurred. Please contact support.",
|
|
70
|
+
},
|
|
71
|
+
500,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { type Context, Hono } from "hono";
|
|
2
|
+
import { stream } from "hono/streaming";
|
|
3
|
+
import { Lobb } from "../../Lobb.ts";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { Query } from "mingo";
|
|
6
|
+
|
|
7
|
+
const eventSubscriptionSchema = z.object({
|
|
8
|
+
event: z.string(),
|
|
9
|
+
filter: z.record(z.unknown()).optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const eventsRequestSchema = z.object({
|
|
13
|
+
subscriptions: z.array(eventSubscriptionSchema).min(1, "At least one subscription is required"),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
interface EventPayload {
|
|
17
|
+
eventName: string;
|
|
18
|
+
payload: Record<string, unknown>;
|
|
19
|
+
timestamp: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getEventsRoute() {
|
|
23
|
+
const rest = new Hono();
|
|
24
|
+
|
|
25
|
+
rest.post("/", async (c: Context) => {
|
|
26
|
+
// Parse and validate request body
|
|
27
|
+
const body = await c.req.json();
|
|
28
|
+
const parseResult = eventsRequestSchema.safeParse(body);
|
|
29
|
+
|
|
30
|
+
if (!parseResult.success) {
|
|
31
|
+
return c.json({ error: parseResult.error.errors }, 400);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { subscriptions } = parseResult.data;
|
|
35
|
+
const lobb = Lobb.instance;
|
|
36
|
+
|
|
37
|
+
// Validate that all requested events exist
|
|
38
|
+
for (const subscription of subscriptions) {
|
|
39
|
+
if (!lobb.eventSystem.eventExists(subscription.event)) {
|
|
40
|
+
return c.json({
|
|
41
|
+
error: `Event '${subscription.event}' does not exist`
|
|
42
|
+
}, 400);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return stream(c, async (stream) => {
|
|
47
|
+
const signal = c.req.raw.signal;
|
|
48
|
+
const subscriptionIds: string[] = [];
|
|
49
|
+
const eventQueue: EventPayload[] = [];
|
|
50
|
+
let isProcessing = false;
|
|
51
|
+
|
|
52
|
+
// Handler that queues events with optional filtering
|
|
53
|
+
const createHandler = (eventName: string, filter?: Record<string, unknown>) => async (input: Record<string, unknown>) => {
|
|
54
|
+
// Apply filter if provided
|
|
55
|
+
if (filter) {
|
|
56
|
+
const query = new Query(filter);
|
|
57
|
+
if (!query.test(input)) {
|
|
58
|
+
return input; // Filter didn't match, don't queue
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
eventQueue.push({
|
|
63
|
+
eventName,
|
|
64
|
+
payload: input,
|
|
65
|
+
timestamp: new Date().toISOString()
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Process queue if not already processing
|
|
69
|
+
if (!isProcessing && !signal.aborted) {
|
|
70
|
+
await processQueue();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return input;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Process queued events
|
|
77
|
+
const processQueue = async () => {
|
|
78
|
+
if (isProcessing) return;
|
|
79
|
+
isProcessing = true;
|
|
80
|
+
|
|
81
|
+
while (eventQueue.length > 0 && !signal.aborted) {
|
|
82
|
+
const event = eventQueue.shift();
|
|
83
|
+
try {
|
|
84
|
+
await stream.write(JSON.stringify(event) + "\n");
|
|
85
|
+
} catch (_error) {
|
|
86
|
+
// Stream closed, stop processing
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
isProcessing = false;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// Send an initial empty object so Bun resolves the client's fetch() promise
|
|
96
|
+
// immediately (Bun waits for the first chunk before resolving a streaming fetch)
|
|
97
|
+
await stream.write("\n");
|
|
98
|
+
|
|
99
|
+
// Subscribe to all requested events with filters
|
|
100
|
+
for (const subscription of subscriptions) {
|
|
101
|
+
const subscriptionId = `stream_${crypto.randomUUID()}`;
|
|
102
|
+
subscriptionIds.push(subscriptionId);
|
|
103
|
+
|
|
104
|
+
lobb.eventSystem.subscribe({
|
|
105
|
+
name: subscriptionId,
|
|
106
|
+
eventName: subscription.event,
|
|
107
|
+
handler: createHandler(subscription.event, subscription.filter),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Keep connection alive and wait for abort signal
|
|
112
|
+
await new Promise((resolve) => {
|
|
113
|
+
signal.addEventListener('abort', () => {
|
|
114
|
+
resolve(null);
|
|
115
|
+
}, { once: true });
|
|
116
|
+
});
|
|
117
|
+
} catch (_error) {
|
|
118
|
+
// Client disconnected - clean exit
|
|
119
|
+
} finally {
|
|
120
|
+
// Clean up all subscriptions
|
|
121
|
+
for (const subscriptionId of subscriptionIds) {
|
|
122
|
+
await lobb.eventSystem.unsubscribe(subscriptionId);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return rest;
|
|
129
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { MetaService } from "./service.ts";
|
|
4
|
+
|
|
5
|
+
export const metaRoutePathDescription: any = {
|
|
6
|
+
"/api/meta": {
|
|
7
|
+
get: {
|
|
8
|
+
summary: `Get Meta`,
|
|
9
|
+
description: "Get meta data of the API",
|
|
10
|
+
tags: [
|
|
11
|
+
"meta",
|
|
12
|
+
],
|
|
13
|
+
responses: {
|
|
14
|
+
200: {
|
|
15
|
+
description: "Successful response",
|
|
16
|
+
content: {
|
|
17
|
+
"application/json": {
|
|
18
|
+
schema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
collections: {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {},
|
|
24
|
+
},
|
|
25
|
+
filter: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {},
|
|
28
|
+
},
|
|
29
|
+
extensions: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {},
|
|
32
|
+
},
|
|
33
|
+
dashboard_navs: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function getMetaRoute() {
|
|
48
|
+
const rest = new Hono();
|
|
49
|
+
|
|
50
|
+
rest.get("/", async (c: Context) => {
|
|
51
|
+
const returnedValue: any = {};
|
|
52
|
+
|
|
53
|
+
returnedValue["version"] = MetaService.getVersion();
|
|
54
|
+
returnedValue["relations"] = MetaService.getRelations();
|
|
55
|
+
returnedValue["collections"] = MetaService.getCollections();
|
|
56
|
+
returnedValue["extensions"] = await MetaService.getExtensions();
|
|
57
|
+
returnedValue["filter"] = MetaService.getFilter();
|
|
58
|
+
returnedValue["studio_workflows"] = MetaService.getStudioWorkflows();
|
|
59
|
+
returnedValue["events"] = MetaService.getEvents();
|
|
60
|
+
returnedValue["event_context_type"] = MetaService.getEventContextType();
|
|
61
|
+
|
|
62
|
+
return c.json(returnedValue);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return rest;
|
|
66
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
basicFilterOperatorsSchema,
|
|
3
|
+
filterJsonSchema,
|
|
4
|
+
filterSchema,
|
|
5
|
+
} from "../../types/index.ts";
|
|
6
|
+
import { Lobb } from "../../Lobb.ts";
|
|
7
|
+
import { convertSchemaToTypeString } from "../../utils/utils.ts";
|
|
8
|
+
import { z, type ZodFunction } from "zod";
|
|
9
|
+
import { LobbErrorSchema } from "../../LobbError.ts";
|
|
10
|
+
import packageJson from "../../../package.json" with { type: "json" };
|
|
11
|
+
|
|
12
|
+
export class MetaService {
|
|
13
|
+
public static getConfig() {
|
|
14
|
+
const { collections } = Lobb.instance.configManager.config;
|
|
15
|
+
const returnedData = { collections };
|
|
16
|
+
return returnedData;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public static getVersion() {
|
|
20
|
+
return packageJson.version;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public static getRelations() {
|
|
24
|
+
const relations = Lobb.instance.configManager.config.relations;
|
|
25
|
+
return relations;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public static getCollections() {
|
|
29
|
+
const collections: any = {};
|
|
30
|
+
|
|
31
|
+
const collectionsNames = Lobb.instance.configManager.getCollectionsNames();
|
|
32
|
+
for (let index = 0; index < collectionsNames.length; index++) {
|
|
33
|
+
const collectionName = collectionsNames[index];
|
|
34
|
+
const collection = Lobb.instance.configManager.getCollection(
|
|
35
|
+
collectionName,
|
|
36
|
+
);
|
|
37
|
+
collections[collectionName] = {};
|
|
38
|
+
|
|
39
|
+
// is collection singleton
|
|
40
|
+
collections[collectionName].singleton = Lobb.instance.configManager
|
|
41
|
+
.isCollectionSingleton(collectionName);
|
|
42
|
+
|
|
43
|
+
// filling the extension property
|
|
44
|
+
collections[collectionName].category = collection.category;
|
|
45
|
+
collections[collectionName].owner = Lobb.instance.utils
|
|
46
|
+
.getCollectionOwner(
|
|
47
|
+
collectionName,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// filling the fields
|
|
51
|
+
collections[collectionName].fields = {};
|
|
52
|
+
const fieldNames = Lobb.instance.configManager.getFieldNames(
|
|
53
|
+
collectionName,
|
|
54
|
+
);
|
|
55
|
+
for (let index = 0; index < fieldNames.length; index++) {
|
|
56
|
+
const fieldName = fieldNames[index];
|
|
57
|
+
const fieldConfig = Lobb.instance.configManager.getField(
|
|
58
|
+
fieldName,
|
|
59
|
+
collectionName,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const field: any = {};
|
|
63
|
+
field.type = fieldConfig.type;
|
|
64
|
+
field.label = fieldName;
|
|
65
|
+
field.key = fieldName;
|
|
66
|
+
field.validators = fieldConfig.validators;
|
|
67
|
+
field.pre_processors = fieldConfig.pre_processors;
|
|
68
|
+
field.ui = fieldConfig.ui;
|
|
69
|
+
|
|
70
|
+
collections[collectionName].fields[fieldName] = field;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return collections;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public static async getExtensions() {
|
|
78
|
+
const extensions: Record<string, any> = {};
|
|
79
|
+
if (Lobb.instance.configManager.config.extensions) {
|
|
80
|
+
const extensionNames = Lobb.instance.configManager.getExtensionNames();
|
|
81
|
+
for (let index = 0; index < extensionNames.length; index++) {
|
|
82
|
+
const extensionName = extensionNames[index];
|
|
83
|
+
const lobb = Lobb.instance;
|
|
84
|
+
const extensionMeta = await lobb.extensionSystem.getExtensionMeta(
|
|
85
|
+
extensionName,
|
|
86
|
+
);
|
|
87
|
+
extensions[extensionName] = extensionMeta;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return extensions;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public static getFilter() {
|
|
95
|
+
const filter: Record<string, any> = {};
|
|
96
|
+
|
|
97
|
+
filter.operators = Object.keys(basicFilterOperatorsSchema.shape);
|
|
98
|
+
// this is needed so that the AI in lobb studio knows how to generate a filter object
|
|
99
|
+
filter.filter_schema = filterJsonSchema;
|
|
100
|
+
|
|
101
|
+
return filter;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public static getStudioWorkflows() {
|
|
105
|
+
const workflows = Lobb.instance.workflowSystem.workflows;
|
|
106
|
+
const studio_workflows = workflows.filter((workflow) => {
|
|
107
|
+
return workflow.eventName?.startsWith("studio.");
|
|
108
|
+
});
|
|
109
|
+
return studio_workflows.map((workflow) => {
|
|
110
|
+
return {
|
|
111
|
+
...workflow,
|
|
112
|
+
handler: workflow.handler.toString(),
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public static getEvents() {
|
|
118
|
+
const events = Lobb.instance.eventSystem.events;
|
|
119
|
+
return events.map((event) => {
|
|
120
|
+
return {
|
|
121
|
+
...event,
|
|
122
|
+
inputSchema: convertSchemaToTypeString(
|
|
123
|
+
"Input",
|
|
124
|
+
event.inputSchema,
|
|
125
|
+
),
|
|
126
|
+
outputSchema: convertSchemaToTypeString(
|
|
127
|
+
"Output",
|
|
128
|
+
event.outputSchema,
|
|
129
|
+
),
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public static getEventContextType() {
|
|
135
|
+
const workflows = Lobb.instance.workflowSystem.workflows;
|
|
136
|
+
|
|
137
|
+
const workflowsObjectSchema: Record<string, ZodFunction<any, any>> = {};
|
|
138
|
+
for (let index = 0; index < workflows.length; index++) {
|
|
139
|
+
const workflow = workflows[index];
|
|
140
|
+
const inputSchema = workflow.inputSchema
|
|
141
|
+
? workflow.inputSchema(z)
|
|
142
|
+
: z.any();
|
|
143
|
+
const outputSchema = workflow.outputSchema
|
|
144
|
+
? workflow.outputSchema(z)
|
|
145
|
+
: z.any();
|
|
146
|
+
workflowsObjectSchema[workflow.name] = z.function().args(inputSchema)
|
|
147
|
+
.returns(z.promise(outputSchema));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const schema = z.object({
|
|
151
|
+
eventName: z.string(),
|
|
152
|
+
workflows: z.object(workflowsObjectSchema),
|
|
153
|
+
LobbError: z.function().args(LobbErrorSchema).returns(z.void()),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const typeString = convertSchemaToTypeString(
|
|
157
|
+
"Context",
|
|
158
|
+
schema,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return typeString;
|
|
162
|
+
}
|
|
163
|
+
}
|