@thangnv-dev/message-streaming-mikroorm-node 0.0.1

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.
@@ -0,0 +1,8 @@
1
+ import { StandardError } from '@thangnv-dev/error-common';
2
+ export declare class MessageStreamingQueryError extends StandardError {
3
+ constructor(operation: string, cause?: unknown);
4
+ }
5
+ export declare class MessageStreamingInvalidResultError extends StandardError {
6
+ constructor(field: string, cause?: unknown);
7
+ }
8
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAIzD,qBAAa,0BAA2B,SAAQ,aAAa;gBAC/C,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAS/C;AAED,qBAAa,kCAAmC,SAAQ,aAAa;gBACvD,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAS3C"}
package/dist/errors.js ADDED
@@ -0,0 +1,25 @@
1
+ import { StandardError } from '@thangnv-dev/error-common';
2
+ const MESSAGE_STREAMING_IMPL_PACKAGE = '@thangnv-dev/message-streaming-mikroorm-node';
3
+ export class MessageStreamingQueryError extends StandardError {
4
+ constructor(operation, cause) {
5
+ super({
6
+ package: MESSAGE_STREAMING_IMPL_PACKAGE,
7
+ code: 'MESSAGE_STREAMING_QUERY_FAILED',
8
+ cause,
9
+ message: `${operation} query failed`,
10
+ });
11
+ this.name = 'MessageStreamingQueryError';
12
+ }
13
+ }
14
+ export class MessageStreamingInvalidResultError extends StandardError {
15
+ constructor(field, cause) {
16
+ super({
17
+ package: MESSAGE_STREAMING_IMPL_PACKAGE,
18
+ code: 'MESSAGE_STREAMING_INVALID_RESULT',
19
+ cause,
20
+ message: `invalid message-streaming result field: ${field}`,
21
+ });
22
+ this.name = 'MessageStreamingInvalidResultError';
23
+ }
24
+ }
25
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAEzD,MAAM,8BAA8B,GAAG,8CAA8C,CAAA;AAErF,MAAM,OAAO,0BAA2B,SAAQ,aAAa;IAC3D,YAAY,SAAiB,EAAE,KAAe;QAC5C,KAAK,CAAC;YACJ,OAAO,EAAE,8BAA8B;YACvC,IAAI,EAAE,gCAAgC;YACtC,KAAK;YACL,OAAO,EAAE,GAAG,SAAS,eAAe;SACrC,CAAC,CAAA;QACF,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAA;IAC1C,CAAC;CACF;AAED,MAAM,OAAO,kCAAmC,SAAQ,aAAa;IACnE,YAAY,KAAa,EAAE,KAAe;QACxC,KAAK,CAAC;YACJ,OAAO,EAAE,8BAA8B;YACvC,IAAI,EAAE,kCAAkC;YACxC,KAAK;YACL,OAAO,EAAE,2CAA2C,KAAK,EAAE;SAC5D,CAAC,CAAA;QACF,IAAI,CAAC,IAAI,GAAG,oCAAoC,CAAA;IAClD,CAAC;CACF"}
@@ -0,0 +1,4 @@
1
+ export { MessageStreamingInvalidResultError, MessageStreamingQueryError } from './errors.js';
2
+ export { KnexMessageStreaming, type KnexMessageStreamingOptions } from './message-streaming.js';
3
+ export { Migration20260215231000MessageStore } from './migrations/migration-20260215231000-message-store.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kCAAkC,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAC5F,OAAO,EAAE,oBAAoB,EAAE,KAAK,2BAA2B,EAAE,MAAM,wBAAwB,CAAA;AAC/F,OAAO,EAAE,mCAAmC,EAAE,MAAM,wDAAwD,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { MessageStreamingInvalidResultError, MessageStreamingQueryError } from './errors.js';
2
+ export { KnexMessageStreaming } from './message-streaming.js';
3
+ export { Migration20260215231000MessageStore } from './migrations/migration-20260215231000-message-store.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kCAAkC,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAC5F,OAAO,EAAE,oBAAoB,EAAoC,MAAM,wBAAwB,CAAA;AAC/F,OAAO,EAAE,mCAAmC,EAAE,MAAM,wDAAwD,CAAA"}
@@ -0,0 +1,18 @@
1
+ import 'temporal-polyfill/global';
2
+ import { type MessageStreaming, type MessageStreamingGetLastStreamMessageInput, type MessageStreamingMessage, type MessageStreamingReadCategoryInput, type MessageStreamingReadStreamInput, type MessageStreamingWriteInput } from '@thangnv-dev/message-streaming-node';
3
+ import type { SqlEntityManager } from '@mikro-orm/knex';
4
+ export type KnexMessageStreamingOptions = {
5
+ readonly schema?: string;
6
+ };
7
+ export declare class KnexMessageStreaming implements MessageStreaming {
8
+ private readonly em;
9
+ private readonly schema;
10
+ constructor(em: SqlEntityManager, options?: KnexMessageStreamingOptions);
11
+ writeMessage<TData = unknown, TMetadata = unknown>(input: MessageStreamingWriteInput<TData, TMetadata>): Promise<bigint>;
12
+ getStreamMessages<TData = unknown, TMetadata = unknown>(input: MessageStreamingReadStreamInput): Promise<Array<MessageStreamingMessage<TData, TMetadata>>>;
13
+ getCategoryMessages<TData = unknown, TMetadata = unknown>(input: MessageStreamingReadCategoryInput): Promise<Array<MessageStreamingMessage<TData, TMetadata>>>;
14
+ getLastStreamMessage<TData = unknown, TMetadata = unknown>(input: MessageStreamingGetLastStreamMessageInput): Promise<MessageStreamingMessage<TData, TMetadata> | null>;
15
+ streamVersion(streamNameInput: string): Promise<bigint | null>;
16
+ private queryRows;
17
+ }
18
+ //# sourceMappingURL=message-streaming.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-streaming.d.ts","sourceRoot":"","sources":["../src/message-streaming.ts"],"names":[],"mappings":"AAAA,OAAO,0BAA0B,CAAA;AACjC,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,yCAAyC,EAC9C,KAAK,uBAAuB,EAC5B,KAAK,iCAAiC,EACtC,KAAK,+BAA+B,EACpC,KAAK,0BAA0B,EAChC,MAAM,qCAAqC,CAAA;AAC5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAmBvD,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAsKD,qBAAa,oBAAqB,YAAW,gBAAgB;IAC3D,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAkB;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;gBAEnB,EAAE,EAAE,gBAAgB,EAAE,OAAO,GAAE,2BAAgC;IAOrE,YAAY,CAAC,KAAK,GAAG,OAAO,EAAE,SAAS,GAAG,OAAO,EACrD,KAAK,EAAE,0BAA0B,CAAC,KAAK,EAAE,SAAS,CAAC,GAClD,OAAO,CAAC,MAAM,CAAC;IAgBZ,iBAAiB,CAAC,KAAK,GAAG,OAAO,EAAE,SAAS,GAAG,OAAO,EAC1D,KAAK,EAAE,+BAA+B,GACrC,OAAO,CAAC,KAAK,CAAC,uBAAuB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;IActD,mBAAmB,CAAC,KAAK,GAAG,OAAO,EAAE,SAAS,GAAG,OAAO,EAC5D,KAAK,EAAE,iCAAiC,GACvC,OAAO,CAAC,KAAK,CAAC,uBAAuB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;IAsBtD,oBAAoB,CAAC,KAAK,GAAG,OAAO,EAAE,SAAS,GAAG,OAAO,EAC7D,KAAK,EAAE,yCAAyC,GAC/C,OAAO,CAAC,uBAAuB,CAAC,KAAK,EAAE,SAAS,CAAC,GAAG,IAAI,CAAC;IAmBtD,aAAa,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;YAiBtD,SAAS;CAYxB"}
@@ -0,0 +1,213 @@
1
+ import 'temporal-polyfill/global';
2
+ import { MESSAGE_STREAMING_DEFAULT_BATCH_SIZE, MESSAGE_STREAMING_DEFAULT_POSITION, } from '@thangnv-dev/message-streaming-node';
3
+ import { MessageStreamingInvalidResultError, MessageStreamingQueryError } from './errors.js';
4
+ const DEFAULT_SCHEMA = 'message_store';
5
+ const SQL_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
6
+ function isRecord(value) {
7
+ return typeof value === 'object' && value !== null;
8
+ }
9
+ function readField(record, keys) {
10
+ for (const key of keys) {
11
+ if (Object.prototype.hasOwnProperty.call(record, key)) {
12
+ return record[key];
13
+ }
14
+ }
15
+ return undefined;
16
+ }
17
+ function requireNonEmptyString(fieldName, value) {
18
+ if (typeof value !== 'string') {
19
+ throw new TypeError(`${fieldName} must be a string`);
20
+ }
21
+ const trimmed = value.trim();
22
+ if (!trimmed) {
23
+ throw new TypeError(`${fieldName} must not be empty`);
24
+ }
25
+ return trimmed;
26
+ }
27
+ function toPgBigInt(value) {
28
+ if (value == null) {
29
+ return null;
30
+ }
31
+ return value.toString();
32
+ }
33
+ function parseBigIntField(fieldName, value) {
34
+ if (typeof value === 'bigint') {
35
+ return value;
36
+ }
37
+ if (typeof value === 'string') {
38
+ try {
39
+ return BigInt(value);
40
+ }
41
+ catch (cause) {
42
+ throw new MessageStreamingInvalidResultError(fieldName, cause);
43
+ }
44
+ }
45
+ if (typeof value === 'number' && Number.isInteger(value)) {
46
+ return BigInt(value);
47
+ }
48
+ throw new MessageStreamingInvalidResultError(fieldName);
49
+ }
50
+ function parseJsonField(fieldName, value) {
51
+ if (value == null) {
52
+ return null;
53
+ }
54
+ if (typeof value === 'string') {
55
+ try {
56
+ return JSON.parse(value);
57
+ }
58
+ catch (cause) {
59
+ throw new MessageStreamingInvalidResultError(fieldName, cause);
60
+ }
61
+ }
62
+ return value;
63
+ }
64
+ function hasTimeZoneSuffix(value) {
65
+ return /(Z|z|[+-]\d{2}:\d{2}|[+-]\d{2})$/.test(value);
66
+ }
67
+ function normalizeUtcTimestamp(value) {
68
+ const normalized = value.trim().replace(' ', 'T');
69
+ if (hasTimeZoneSuffix(normalized)) {
70
+ return normalized;
71
+ }
72
+ return `${normalized}Z`;
73
+ }
74
+ function parseInstantField(fieldName, value) {
75
+ if (value instanceof Date) {
76
+ return Temporal.Instant.from(value.toISOString());
77
+ }
78
+ try {
79
+ if (typeof value === 'string') {
80
+ return Temporal.Instant.from(normalizeUtcTimestamp(value));
81
+ }
82
+ return Temporal.Instant.from(value);
83
+ }
84
+ catch (cause) {
85
+ throw new MessageStreamingInvalidResultError(fieldName, cause);
86
+ }
87
+ }
88
+ function toMessageStreamingMessage(rowInput) {
89
+ if (!isRecord(rowInput)) {
90
+ throw new MessageStreamingInvalidResultError('row');
91
+ }
92
+ const id = requireNonEmptyString('id', readField(rowInput, ['id']));
93
+ const streamName = requireNonEmptyString('stream_name', readField(rowInput, ['stream_name', 'streamName']));
94
+ const messageType = requireNonEmptyString('type', readField(rowInput, ['type']));
95
+ const position = parseBigIntField('position', readField(rowInput, ['position']));
96
+ const globalPosition = parseBigIntField('global_position', readField(rowInput, ['global_position', 'globalPosition']));
97
+ const data = parseJsonField('data', readField(rowInput, ['data']));
98
+ const metadata = parseJsonField('metadata', readField(rowInput, ['metadata']));
99
+ const time = parseInstantField('time', readField(rowInput, ['time']));
100
+ return {
101
+ id,
102
+ streamName,
103
+ type: messageType,
104
+ position,
105
+ globalPosition,
106
+ data,
107
+ metadata,
108
+ time,
109
+ };
110
+ }
111
+ function extractRows(rawResult) {
112
+ if (isRecord(rawResult)) {
113
+ const rows = rawResult.rows;
114
+ if (Array.isArray(rows)) {
115
+ return rows;
116
+ }
117
+ }
118
+ if (Array.isArray(rawResult)) {
119
+ if (rawResult.length === 0 || !Array.isArray(rawResult[0])) {
120
+ return rawResult;
121
+ }
122
+ const [rows] = rawResult;
123
+ if (Array.isArray(rows)) {
124
+ return rows;
125
+ }
126
+ }
127
+ return [];
128
+ }
129
+ function serializeJson(fieldName, value) {
130
+ try {
131
+ const serialized = JSON.stringify(value);
132
+ if (serialized === undefined) {
133
+ throw new TypeError(`${fieldName} must be JSON-serializable`);
134
+ }
135
+ return serialized;
136
+ }
137
+ catch (cause) {
138
+ throw new TypeError(`${fieldName} must be JSON-serializable`, { cause });
139
+ }
140
+ }
141
+ function assertValidSchemaIdentifier(schema) {
142
+ if (!SQL_IDENTIFIER_PATTERN.test(schema)) {
143
+ throw new TypeError('schema must be a valid SQL identifier');
144
+ }
145
+ }
146
+ export class KnexMessageStreaming {
147
+ em;
148
+ schema;
149
+ constructor(em, options = {}) {
150
+ this.em = em;
151
+ const schema = options.schema ?? DEFAULT_SCHEMA;
152
+ assertValidSchemaIdentifier(schema);
153
+ this.schema = schema;
154
+ }
155
+ async writeMessage(input) {
156
+ const id = requireNonEmptyString('id', input.id);
157
+ const streamName = requireNonEmptyString('streamName', input.streamName);
158
+ const type = requireNonEmptyString('type', input.type);
159
+ const dataJson = serializeJson('data', input.data);
160
+ const metadataJson = input.metadata == null ? null : serializeJson('metadata', input.metadata);
161
+ const rows = await this.queryRows(`select ${this.schema}.write_message(?, ?, ?, ?::jsonb, ?::jsonb, ?::bigint) as write_message`, [id, streamName, type, dataJson, metadataJson, toPgBigInt(input.expectedVersion ?? null)], 'writeMessage');
162
+ return parseBigIntField('write_message', rows[0]?.write_message);
163
+ }
164
+ async getStreamMessages(input) {
165
+ const streamName = requireNonEmptyString('streamName', input.streamName);
166
+ const position = input.position ?? MESSAGE_STREAMING_DEFAULT_POSITION;
167
+ const batchSize = input.batchSize ?? MESSAGE_STREAMING_DEFAULT_BATCH_SIZE;
168
+ const rows = await this.queryRows(`select * from ${this.schema}.get_stream_messages(?, ?::bigint, ?::bigint, ?)`, [streamName, toPgBigInt(position), toPgBigInt(batchSize), input.condition ?? null], 'getStreamMessages');
169
+ return rows.map((row) => toMessageStreamingMessage(row));
170
+ }
171
+ async getCategoryMessages(input) {
172
+ const category = requireNonEmptyString('category', input.category);
173
+ const position = input.position ?? MESSAGE_STREAMING_DEFAULT_POSITION;
174
+ const batchSize = input.batchSize ?? MESSAGE_STREAMING_DEFAULT_BATCH_SIZE;
175
+ const rows = await this.queryRows(`select * from ${this.schema}.get_category_messages(?, ?::bigint, ?::bigint, ?, ?::bigint, ?::bigint, ?)`, [
176
+ category,
177
+ toPgBigInt(position),
178
+ toPgBigInt(batchSize),
179
+ input.correlation ?? null,
180
+ toPgBigInt(input.consumerGroupMember ?? null),
181
+ toPgBigInt(input.consumerGroupSize ?? null),
182
+ input.condition ?? null,
183
+ ], 'getCategoryMessages');
184
+ return rows.map((row) => toMessageStreamingMessage(row));
185
+ }
186
+ async getLastStreamMessage(input) {
187
+ const streamName = requireNonEmptyString('streamName', input.streamName);
188
+ const rows = input.type
189
+ ? await this.queryRows(`select * from ${this.schema}.get_last_stream_message(?, ?)`, [streamName, input.type], 'getLastStreamMessage')
190
+ : await this.queryRows(`select * from ${this.schema}.get_last_stream_message(?)`, [streamName], 'getLastStreamMessage');
191
+ const row = rows[0];
192
+ return row ? toMessageStreamingMessage(row) : null;
193
+ }
194
+ async streamVersion(streamNameInput) {
195
+ const streamName = requireNonEmptyString('streamName', streamNameInput);
196
+ const rows = await this.queryRows(`select ${this.schema}.stream_version(?) as stream_version`, [streamName], 'streamVersion');
197
+ const streamVersion = rows[0]?.stream_version;
198
+ if (streamVersion == null) {
199
+ return null;
200
+ }
201
+ return parseBigIntField('stream_version', streamVersion);
202
+ }
203
+ async queryRows(sql, bindings, operation) {
204
+ try {
205
+ const rawResult = await this.em.execute(sql, [...bindings], 'all');
206
+ return extractRows(rawResult);
207
+ }
208
+ catch (cause) {
209
+ throw new MessageStreamingQueryError(operation, cause);
210
+ }
211
+ }
212
+ }
213
+ //# sourceMappingURL=message-streaming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-streaming.js","sourceRoot":"","sources":["../src/message-streaming.ts"],"names":[],"mappings":"AAAA,OAAO,0BAA0B,CAAA;AACjC,OAAO,EACL,oCAAoC,EACpC,kCAAkC,GAOnC,MAAM,qCAAqC,CAAA;AAE5C,OAAO,EAAE,kCAAkC,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAE5F,MAAM,cAAc,GAAG,eAAe,CAAA;AACtC,MAAM,sBAAsB,GAAG,0BAA0B,CAAA;AAmBzD,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAA;AACpD,CAAC;AAED,SAAS,SAAS,CAAC,MAA+B,EAAE,IAAuB;IACzE,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;YACtD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAA;QACpB,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,SAAS,qBAAqB,CAAC,SAAiB,EAAE,KAAc;IAC9D,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,IAAI,SAAS,CAAC,GAAG,SAAS,mBAAmB,CAAC,CAAA;IACtD,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;IAC5B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,SAAS,CAAC,GAAG,SAAS,oBAAoB,CAAC,CAAA;IACvD,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,SAAS,UAAU,CAAC,KAAgC;IAClD,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;QAClB,OAAO,IAAI,CAAA;IACb,CAAC;IACD,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAA;AACzB,CAAC;AAED,SAAS,gBAAgB,CAAC,SAAiB,EAAE,KAAc;IACzD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;QACtB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,kCAAkC,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;QAChE,CAAC;IACH,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;QACzD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;IACtB,CAAC;IACD,MAAM,IAAI,kCAAkC,CAAC,SAAS,CAAC,CAAA;AACzD,CAAC;AAED,SAAS,cAAc,CAAI,SAAiB,EAAE,KAAc;IAC1D,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;QAClB,OAAO,IAAI,CAAA;IACb,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAM,CAAA;QAC/B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,kCAAkC,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;QAChE,CAAC;IACH,CAAC;IACD,OAAO,KAAU,CAAA;AACnB,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAa;IACtC,OAAO,kCAAkC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AACvD,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAa;IAC1C,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IACjD,IAAI,iBAAiB,CAAC,UAAU,CAAC,EAAE,CAAC;QAClC,OAAO,UAAU,CAAA;IACnB,CAAC;IACD,OAAO,GAAG,UAAU,GAAG,CAAA;AACzB,CAAC;AAED,SAAS,iBAAiB,CAAC,SAAiB,EAAE,KAAc;IAC1D,IAAI,KAAK,YAAY,IAAI,EAAE,CAAC;QAC1B,OAAO,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAA;IACnD,CAAC;IAED,IAAI,CAAC;QACH,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,OAAO,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAA;QAC5D,CAAC;QACD,OAAO,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,KAAyB,CAAC,CAAA;IACzD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,kCAAkC,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;IAChE,CAAC;AACH,CAAC;AAED,SAAS,yBAAyB,CAChC,QAAiB;IAEjB,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,kCAAkC,CAAC,KAAK,CAAC,CAAA;IACrD,CAAC;IAED,MAAM,EAAE,GAAG,qBAAqB,CAAC,IAAI,EAAE,SAAS,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACnE,MAAM,UAAU,GAAG,qBAAqB,CACtC,aAAa,EACb,SAAS,CAAC,QAAQ,EAAE,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC,CACnD,CAAA;IACD,MAAM,WAAW,GAAG,qBAAqB,CAAC,MAAM,EAAE,SAAS,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAChF,MAAM,QAAQ,GAAG,gBAAgB,CAAC,UAAU,EAAE,SAAS,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IAChF,MAAM,cAAc,GAAG,gBAAgB,CACrC,iBAAiB,EACjB,SAAS,CAAC,QAAQ,EAAE,CAAC,iBAAiB,EAAE,gBAAgB,CAAC,CAAC,CAC3D,CAAA;IACD,MAAM,IAAI,GAAG,cAAc,CAAQ,MAAM,EAAE,SAAS,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IACzE,MAAM,QAAQ,GAAG,cAAc,CAAY,UAAU,EAAE,SAAS,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IACzF,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,EAAE,SAAS,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAErE,OAAO;QACL,EAAE;QACF,UAAU;QACV,IAAI,EAAE,WAAW;QACjB,QAAQ;QACR,cAAc;QACd,IAAI;QACJ,QAAQ;QACR,IAAI;KACL,CAAA;AACH,CAAC;AAED,SAAS,WAAW,CAAO,SAAkB;IAC3C,IAAI,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAA;QAC3B,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,OAAO,IAAc,CAAA;QACvB,CAAC;IACH,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7B,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3D,OAAO,SAAmB,CAAA;QAC5B,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,GAAG,SAAS,CAAA;QACxB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,OAAO,IAAc,CAAA;QACvB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,CAAA;AACX,CAAC;AAED,SAAS,aAAa,CAAC,SAAiB,EAAE,KAAc;IACtD,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;QACxC,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,SAAS,CAAC,GAAG,SAAS,4BAA4B,CAAC,CAAA;QAC/D,CAAC;QACD,OAAO,UAAU,CAAA;IACnB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,SAAS,CAAC,GAAG,SAAS,4BAA4B,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;IAC1E,CAAC;AACH,CAAC;AAED,SAAS,2BAA2B,CAAC,MAAc;IACjD,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,SAAS,CAAC,uCAAuC,CAAC,CAAA;IAC9D,CAAC;AACH,CAAC;AAED,MAAM,OAAO,oBAAoB;IACd,EAAE,CAAkB;IACpB,MAAM,CAAQ;IAE/B,YAAY,EAAoB,EAAE,UAAuC,EAAE;QACzE,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;QACZ,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,cAAc,CAAA;QAC/C,2BAA2B,CAAC,MAAM,CAAC,CAAA;QACnC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IACtB,CAAC;IAED,KAAK,CAAC,YAAY,CAChB,KAAmD;QAEnD,MAAM,EAAE,GAAG,qBAAqB,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAA;QAChD,MAAM,UAAU,GAAG,qBAAqB,CAAC,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;QACxE,MAAM,IAAI,GAAG,qBAAqB,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;QACtD,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;QAClD,MAAM,YAAY,GAAG,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;QAE9F,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,CAC/B,UAAU,IAAI,CAAC,MAAM,yEAAyE,EAC9F,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAU,CAAC,KAAK,CAAC,eAAe,IAAI,IAAI,CAAC,CAAC,EACzF,cAAc,CACf,CAAA;QAED,OAAO,gBAAgB,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,aAAa,CAAC,CAAA;IAClE,CAAC;IAED,KAAK,CAAC,iBAAiB,CACrB,KAAsC;QAEtC,MAAM,UAAU,GAAG,qBAAqB,CAAC,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;QACxE,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,IAAI,kCAAkC,CAAA;QACrE,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,IAAI,oCAAoC,CAAA;QAEzE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,CAC/B,iBAAiB,IAAI,CAAC,MAAM,kDAAkD,EAC9E,CAAC,UAAU,EAAE,UAAU,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,EAClF,mBAAmB,CACpB,CAAA;QAED,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,yBAAyB,CAAmB,GAAG,CAAC,CAAC,CAAA;IAC5E,CAAC;IAED,KAAK,CAAC,mBAAmB,CACvB,KAAwC;QAExC,MAAM,QAAQ,GAAG,qBAAqB,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;QAClE,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,IAAI,kCAAkC,CAAA;QACrE,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,IAAI,oCAAoC,CAAA;QAEzE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,CAC/B,iBAAiB,IAAI,CAAC,MAAM,6EAA6E,EACzG;YACE,QAAQ;YACR,UAAU,CAAC,QAAQ,CAAC;YACpB,UAAU,CAAC,SAAS,CAAC;YACrB,KAAK,CAAC,WAAW,IAAI,IAAI;YACzB,UAAU,CAAC,KAAK,CAAC,mBAAmB,IAAI,IAAI,CAAC;YAC7C,UAAU,CAAC,KAAK,CAAC,iBAAiB,IAAI,IAAI,CAAC;YAC3C,KAAK,CAAC,SAAS,IAAI,IAAI;SACxB,EACD,qBAAqB,CACtB,CAAA;QAED,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,yBAAyB,CAAmB,GAAG,CAAC,CAAC,CAAA;IAC5E,CAAC;IAED,KAAK,CAAC,oBAAoB,CACxB,KAAgD;QAEhD,MAAM,UAAU,GAAG,qBAAqB,CAAC,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;QAExE,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI;YACrB,CAAC,CAAC,MAAM,IAAI,CAAC,SAAS,CAClB,iBAAiB,IAAI,CAAC,MAAM,gCAAgC,EAC5D,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,EACxB,sBAAsB,CACvB;YACH,CAAC,CAAC,MAAM,IAAI,CAAC,SAAS,CAClB,iBAAiB,IAAI,CAAC,MAAM,6BAA6B,EACzD,CAAC,UAAU,CAAC,EACZ,sBAAsB,CACvB,CAAA;QAEL,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;QACnB,OAAO,GAAG,CAAC,CAAC,CAAC,yBAAyB,CAAmB,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACtE,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,eAAuB;QACzC,MAAM,UAAU,GAAG,qBAAqB,CAAC,YAAY,EAAE,eAAe,CAAC,CAAA;QAEvE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,CAC/B,UAAU,IAAI,CAAC,MAAM,sCAAsC,EAC3D,CAAC,UAAU,CAAC,EACZ,eAAe,CAChB,CAAA;QAED,MAAM,aAAa,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,cAAc,CAAA;QAC7C,IAAI,aAAa,IAAI,IAAI,EAAE,CAAC;YAC1B,OAAO,IAAI,CAAA;QACb,CAAC;QAED,OAAO,gBAAgB,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAA;IAC1D,CAAC;IAEO,KAAK,CAAC,SAAS,CACrB,GAAW,EACX,QAA4B,EAC5B,SAAiB;QAEjB,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,GAAG,QAAQ,CAAC,EAAE,KAAK,CAAC,CAAA;YAClE,OAAO,WAAW,CAAO,SAAS,CAAC,CAAA;QACrC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,0BAA0B,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;QACxD,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ export declare class Migration20260215231000MessageStore extends Migration {
3
+ up(): Promise<void>;
4
+ down(): Promise<void>;
5
+ }
6
+ //# sourceMappingURL=migration-20260215231000-message-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-20260215231000-message-store.d.ts","sourceRoot":"","sources":["../../src/migrations/migration-20260215231000-message-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAEjD,qBAAa,mCAAoC,SAAQ,SAAS;IACjD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IA0InB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAerC"}
@@ -0,0 +1,144 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ export class Migration20260215231000MessageStore extends Migration {
3
+ async up() {
4
+ this.addSql('create schema if not exists message_store;');
5
+ this.addSql(`
6
+ create table if not exists message_store.messages (
7
+ id text primary key,
8
+ stream_name text not null,
9
+ type text not null,
10
+ position bigint not null,
11
+ global_position bigserial not null,
12
+ data jsonb not null,
13
+ metadata jsonb,
14
+ time timestamptz not null default clock_timestamp(),
15
+ unique (stream_name, position)
16
+ );
17
+ `);
18
+ this.addSql(`
19
+ create or replace function message_store.write_message(
20
+ p_id varchar,
21
+ p_stream_name varchar,
22
+ p_type varchar,
23
+ p_data jsonb,
24
+ p_metadata jsonb default null,
25
+ p_expected_version bigint default null
26
+ )
27
+ returns bigint
28
+ language plpgsql
29
+ as $$
30
+ declare
31
+ current_version bigint;
32
+ next_position bigint;
33
+ begin
34
+ select max(m.position)
35
+ into current_version
36
+ from message_store.messages m
37
+ where m.stream_name = p_stream_name;
38
+
39
+ if current_version is null then
40
+ current_version := -1;
41
+ end if;
42
+
43
+ if p_expected_version is not null and current_version <> p_expected_version then
44
+ raise exception 'Wrong expected version. expected %, actual %', p_expected_version, current_version;
45
+ end if;
46
+
47
+ next_position := current_version + 1;
48
+
49
+ insert into message_store.messages (id, stream_name, type, position, data, metadata)
50
+ values (p_id, p_stream_name, p_type, next_position, p_data, p_metadata);
51
+
52
+ return next_position;
53
+ end;
54
+ $$;
55
+ `);
56
+ this.addSql(`
57
+ create or replace function message_store.get_stream_messages(
58
+ p_stream_name varchar,
59
+ p_position bigint default 0,
60
+ p_batch_size bigint default 1000,
61
+ p_condition varchar default null
62
+ )
63
+ returns setof message_store.messages
64
+ language sql
65
+ stable
66
+ as $$
67
+ select m.*
68
+ from message_store.messages m
69
+ where m.stream_name = p_stream_name
70
+ and m.position >= p_position
71
+ and (p_condition is null or m.type = p_condition)
72
+ order by m.position asc
73
+ limit p_batch_size;
74
+ $$;
75
+ `);
76
+ this.addSql(`
77
+ create or replace function message_store.get_category_messages(
78
+ p_category varchar,
79
+ p_position bigint default 0,
80
+ p_batch_size bigint default 1000,
81
+ p_correlation varchar default null,
82
+ p_consumer_group_member bigint default null,
83
+ p_consumer_group_size bigint default null,
84
+ p_condition varchar default null
85
+ )
86
+ returns setof message_store.messages
87
+ language sql
88
+ stable
89
+ as $$
90
+ select m.*
91
+ from message_store.messages m
92
+ where split_part(m.stream_name, '-', 1) = p_category
93
+ and m.global_position >= p_position
94
+ and (p_correlation is null or coalesce(m.metadata ->> 'correlation', '') = p_correlation)
95
+ and (
96
+ p_consumer_group_member is null
97
+ or p_consumer_group_size is null
98
+ or mod(m.global_position, p_consumer_group_size) = p_consumer_group_member
99
+ )
100
+ and (p_condition is null or m.type = p_condition)
101
+ order by m.global_position asc
102
+ limit p_batch_size;
103
+ $$;
104
+ `);
105
+ this.addSql(`
106
+ create or replace function message_store.get_last_stream_message(
107
+ p_stream_name varchar,
108
+ p_type varchar default null
109
+ )
110
+ returns setof message_store.messages
111
+ language sql
112
+ stable
113
+ as $$
114
+ select m.*
115
+ from message_store.messages m
116
+ where m.stream_name = p_stream_name
117
+ and (p_type is null or m.type = p_type)
118
+ order by m.position desc
119
+ limit 1;
120
+ $$;
121
+ `);
122
+ this.addSql(`
123
+ create or replace function message_store.stream_version(p_stream_name varchar)
124
+ returns bigint
125
+ language sql
126
+ stable
127
+ as $$
128
+ select max(m.position)
129
+ from message_store.messages m
130
+ where m.stream_name = p_stream_name;
131
+ $$;
132
+ `);
133
+ }
134
+ async down() {
135
+ this.addSql('drop function if exists message_store.stream_version(varchar);');
136
+ this.addSql('drop function if exists message_store.get_last_stream_message(varchar, varchar);');
137
+ this.addSql('drop function if exists message_store.get_category_messages(varchar, bigint, bigint, varchar, bigint, bigint, varchar);');
138
+ this.addSql('drop function if exists message_store.get_stream_messages(varchar, bigint, bigint, varchar);');
139
+ this.addSql('drop function if exists message_store.write_message(varchar, varchar, varchar, jsonb, jsonb, bigint);');
140
+ this.addSql('drop table if exists message_store.messages;');
141
+ this.addSql('drop schema if exists message_store;');
142
+ }
143
+ }
144
+ //# sourceMappingURL=migration-20260215231000-message-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-20260215231000-message-store.js","sourceRoot":"","sources":["../../src/migrations/migration-20260215231000-message-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAEjD,MAAM,OAAO,mCAAoC,SAAQ,SAAS;IACvD,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC,4CAA4C,CAAC,CAAA;QAEzD,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;KAYX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAqCX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;KAmBX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA4BX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;KAgBX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;KAUX,CAAC,CAAA;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,gEAAgE,CAAC,CAAA;QAC7E,IAAI,CAAC,MAAM,CAAC,kFAAkF,CAAC,CAAA;QAC/F,IAAI,CAAC,MAAM,CACT,yHAAyH,CAC1H,CAAA;QACD,IAAI,CAAC,MAAM,CACT,8FAA8F,CAC/F,CAAA;QACD,IAAI,CAAC,MAAM,CACT,uGAAuG,CACxG,CAAA;QACD,IAAI,CAAC,MAAM,CAAC,8CAA8C,CAAC,CAAA;QAC3D,IAAI,CAAC,MAAM,CAAC,sCAAsC,CAAC,CAAA;IACrD,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@thangnv-dev/message-streaming-mikroorm-node",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.build.json",
22
+ "lint": "eslint .",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "vitest run"
25
+ },
26
+ "dependencies": {
27
+ "@thangnv-dev/error-common": "workspace:^",
28
+ "@thangnv-dev/message-streaming-node": "workspace:^"
29
+ },
30
+ "devDependencies": {
31
+ "@mikro-orm/core": "^6.6.7",
32
+ "@mikro-orm/knex": "^6.6.7",
33
+ "@mikro-orm/migrations": "^6.6.7",
34
+ "@mikro-orm/postgresql": "^6.6.7",
35
+ "@thangnv-dev/mikroorm-nest": "workspace:^",
36
+ "@typescript-eslint/eslint-plugin": "^8.56.0",
37
+ "@typescript-eslint/parser": "^8.56.0",
38
+ "eslint": "^10.0.0",
39
+ "pg": "^8.18.0",
40
+ "temporal-polyfill": "^0.3.0",
41
+ "typescript": "^5.9.3",
42
+ "vitest": "^4.0.18"
43
+ },
44
+ "peerDependencies": {
45
+ "@mikro-orm/knex": "^6.6.6",
46
+ "@mikro-orm/migrations": "^6.6.6",
47
+ "temporal-polyfill": "*"
48
+ }
49
+ }