@genesislcap/mock-server 15.51.1 → 15.53.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.
Files changed (70) hide show
  1. package/README.md +408 -39
  2. package/dist/db/criteria.d.ts +13 -0
  3. package/dist/db/criteria.js +60 -21
  4. package/dist/db/identity.d.ts +11 -0
  5. package/dist/db/identity.js +46 -0
  6. package/dist/db/ordering.d.ts +8 -0
  7. package/dist/db/ordering.js +49 -0
  8. package/dist/db/schema.d.ts +62 -0
  9. package/dist/db/schema.js +490 -0
  10. package/dist/db/store.d.ts +20 -3
  11. package/dist/db/store.js +300 -50
  12. package/dist/handlers/commitEvent.js +47 -9
  13. package/dist/handlers/crudEvents.d.ts +2 -0
  14. package/dist/handlers/crudEvents.js +138 -0
  15. package/dist/handlers/dataLogon.js +105 -6
  16. package/dist/handlers/eventValidation.d.ts +5 -0
  17. package/dist/handlers/eventValidation.js +29 -0
  18. package/dist/handlers/jsonSchema.d.ts +4 -2
  19. package/dist/handlers/jsonSchema.js +222 -61
  20. package/dist/handlers/meta.d.ts +6 -1
  21. package/dist/handlers/meta.js +168 -21
  22. package/dist/handlers/requestReply.js +519 -30
  23. package/dist/handlers/resourceAuth.d.ts +1 -0
  24. package/dist/handlers/resourceAuth.js +14 -0
  25. package/dist/handlers/resources.js +3 -0
  26. package/dist/index.d.ts +3 -2
  27. package/dist/index.js +2 -1
  28. package/dist/protocol/connection.d.ts +2 -0
  29. package/dist/protocol/errors.d.ts +100 -2
  30. package/dist/protocol/errors.js +300 -12
  31. package/dist/protocol/fieldTypes.d.ts +10 -0
  32. package/dist/protocol/fieldTypes.js +107 -0
  33. package/dist/protocol/gsfSchemas.d.ts +9 -0
  34. package/dist/protocol/gsfSchemas.js +423 -0
  35. package/dist/protocol/messageTypes.d.ts +1 -0
  36. package/dist/protocol/messageTypes.js +1 -0
  37. package/dist/protocol/msgNack.d.ts +2 -0
  38. package/dist/protocol/msgNack.js +10 -0
  39. package/dist/protocol/rowUpdate.d.ts +5 -3
  40. package/dist/protocol/rowUpdate.js +33 -15
  41. package/dist/protocol/schemaValidation.d.ts +11 -0
  42. package/dist/protocol/schemaValidation.js +186 -0
  43. package/dist/server.js +95 -29
  44. package/dist/types.d.ts +45 -0
  45. package/package.json +1 -1
  46. package/src/db/criteria.ts +72 -21
  47. package/src/db/identity.ts +53 -0
  48. package/src/db/ordering.ts +56 -0
  49. package/src/db/schema.ts +672 -0
  50. package/src/db/store.ts +343 -50
  51. package/src/handlers/commitEvent.ts +54 -10
  52. package/src/handlers/crudEvents.ts +181 -0
  53. package/src/handlers/dataLogon.ts +127 -7
  54. package/src/handlers/eventValidation.ts +43 -0
  55. package/src/handlers/jsonSchema.ts +302 -69
  56. package/src/handlers/meta.ts +227 -23
  57. package/src/handlers/requestReply.ts +626 -33
  58. package/src/handlers/resourceAuth.ts +18 -0
  59. package/src/handlers/resources.ts +3 -0
  60. package/src/index.ts +17 -1
  61. package/src/protocol/connection.ts +10 -2
  62. package/src/protocol/errors.ts +361 -13
  63. package/src/protocol/fieldTypes.ts +115 -0
  64. package/src/protocol/gsfSchemas.ts +505 -0
  65. package/src/protocol/messageTypes.ts +1 -0
  66. package/src/protocol/msgNack.ts +12 -0
  67. package/src/protocol/rowUpdate.ts +40 -20
  68. package/src/protocol/schemaValidation.ts +208 -0
  69. package/src/server.ts +106 -28
  70. package/src/types.ts +187 -5
@@ -0,0 +1,181 @@
1
+ // The CRUD event handlers a table registers through TableDef.events: the
2
+ // ones Genesis Create generates for a table's `crud` event handlers
3
+ // (codegen-service JsonToKtsEventHandlersGenerator) — eventHandler<Table>
4
+ // "<TABLE>_INSERT" / "_MODIFY" calling entityDb.insert / modify, and
5
+ // eventHandler<Table.ById> "<TABLE>_DELETE" calling entityDb.delete, with no
6
+ // onValidate. They are what the showcase server runs (its recordings come
7
+ // from them) and what Create's real tier deploys. genesis-eventhandler's own
8
+ // GenericInsert/Modify/DeleteEventHandler (EventHandlerBuilder
9
+ // .registerCUDEvents) differ in GENERATED and VALIDATE; see the README. The
10
+ // inbound schema check runs before them, as for any event
11
+ // (eventValidation.ts).
12
+
13
+ import { RECORD_ID, TIMESTAMP } from '../db/identity.ts';
14
+ import {
15
+ crudEventOf,
16
+ generatedFieldList,
17
+ pkFieldsOf,
18
+ primaryKeyIndexName,
19
+ secondaryUniqueIndexes,
20
+ } from '../db/schema.ts';
21
+ import type { Store } from '../db/store.ts';
22
+ import { NackError } from '../protocol/errors.ts';
23
+ import type {
24
+ CrudEventVerb,
25
+ EventHandler,
26
+ EventHandlerCtx,
27
+ EventHandlerResult,
28
+ MockServerConfig,
29
+ Row,
30
+ TableDef,
31
+ } from '../types.ts';
32
+
33
+ const isMissing = (value: unknown) => value === undefined || value === null;
34
+
35
+ // What DUPLICATE_KEY's FIELD shows after '<INDEX> -> ': the index's values,
36
+ // nulls left out, joined with ' | ' (DbUtil.CREATE_DUPLICATE_KEY_ERROR_MESSAGE).
37
+ function indexValueText(fields: string[], row: Row): string {
38
+ return fields
39
+ .map((field) => row[field])
40
+ .filter((value) => !isMissing(value))
41
+ .map(String)
42
+ .join(' | ');
43
+ }
44
+
45
+ // The database's unique constraints: the primary key (unless `self` is the
46
+ // record being modified, whose key can't change) and every other unique
47
+ // index. A null in an index's fields never clashes, as in SQL.
48
+ function checkUnique(table: string, def: TableDef, row: Row, store: Store, self?: string): void {
49
+ const pkFields = pkFieldsOf(def);
50
+ if (self === undefined && pkFields.every((field) => !isMissing(row[field]))) {
51
+ if (store.getRow(table, store.rowKey(table, row))) {
52
+ throw NackError.duplicateKey(
53
+ table,
54
+ primaryKeyIndexName(table, def),
55
+ indexValueText(pkFields, row),
56
+ );
57
+ }
58
+ }
59
+ for (const index of secondaryUniqueIndexes(def)) {
60
+ if (index.fields.some((field) => isMissing(row[field]))) continue;
61
+ const clash = store
62
+ .getAllRows(table)
63
+ .some(
64
+ (other) =>
65
+ store.rowKey(table, other) !== self &&
66
+ index.fields.every((field) => other[field] === row[field]),
67
+ );
68
+ if (clash) throw NackError.duplicateKey(table, index.name, indexValueText(index.fields, row));
69
+ }
70
+ }
71
+
72
+ const declaredNames = (def: TableDef) => def.fields?.map((field) => field.name);
73
+
74
+ // The record an event's DETAILS deserialize into: a declared table's
75
+ // generated DAO gives every field the DETAILS leave out its default, or null
76
+ // (a missing not-null field without one never gets here: the schema check
77
+ // NACKs it). Generated fields stay unset for the database to fill — or, on a
78
+ // modify, to keep. An undeclared table has no DAO to go by: an insert takes
79
+ // DETAILS as they are, and a modify resets the record's other stored columns.
80
+ function defaultsFor(def: TableDef, details: Row, fieldNames: string[]): Row {
81
+ const generated = new Set(generatedFieldList(def));
82
+ const pkFields = pkFieldsOf(def);
83
+ const record: Row = { ...details };
84
+ for (const name of fieldNames) {
85
+ if (Object.hasOwn(details, name) || generated.has(name) || pkFields.includes(name)) continue;
86
+ if (name === RECORD_ID || name === TIMESTAMP) continue;
87
+ const declared = def.fields?.find((field) => field.name === name);
88
+ record[name] = declared?.default !== undefined ? declared.default : null;
89
+ }
90
+ return record;
91
+ }
92
+
93
+ // entityDb.insert(details), then ack(listOf(mapOf(<each generated field,
94
+ // sorted by name> to insertedRow.record.<field>))): every generated field's
95
+ // stored value, supplied or generated — [] for a table with none. With no
96
+ // onValidate, VALIDATE acks without touching the database.
97
+ function insert(
98
+ table: string,
99
+ def: TableDef,
100
+ details: Row,
101
+ ctx: EventHandlerCtx,
102
+ ): EventHandlerResult {
103
+ if (ctx.validate) return {};
104
+ const { store } = ctx;
105
+ const record = defaultsFor(def, details, declaredNames(def) ?? []);
106
+ const row = store.withGeneratedValues(table, record);
107
+ checkUnique(table, def, row, store);
108
+ const inserted = store.insertRow(table, row);
109
+ ctx.broadcastTableChange(table, 'INSERT', store.rowKey(table, inserted));
110
+ const generatedFields = generatedFieldList(def).sort();
111
+ if (generatedFields.length === 0) return {};
112
+ return {
113
+ generated: [
114
+ Object.fromEntries(generatedFields.map((field) => [field, inserted[field] ?? null])),
115
+ ],
116
+ };
117
+ }
118
+
119
+ // entityDb.modify(details): the record is found by its primary key
120
+ // (RECORD_NOT_FOUND naming the key's index, recorded) and DETAILS are written
121
+ // over it. DETAILS are the whole record: a field they leave out is reset to
122
+ // its default or null, as the deserialized DAO's value overwrites the stored
123
+ // one (recorded: a TRADE modify without CREATED_AT, ENTERED_BY and
124
+ // TRADE_STATUS nulls them). Generated fields left out keep their values;
125
+ // RECORD_ID stays, TIMESTAMP moves on. VALIDATE acks without a lookup.
126
+ function modify(
127
+ table: string,
128
+ def: TableDef,
129
+ details: Row,
130
+ ctx: EventHandlerCtx,
131
+ ): EventHandlerResult {
132
+ if (ctx.validate) return {};
133
+ const { store } = ctx;
134
+ const key = store.rowKey(table, details);
135
+ const found = store.getRow(table, key);
136
+ if (!found) throw NackError.recordNotFound(table, primaryKeyIndexName(table, def));
137
+ const patch = defaultsFor(def, details, declaredNames(def) ?? Object.keys(found));
138
+ checkUnique(table, def, { ...found, ...patch }, store, key);
139
+ store.updateRow(table, key, patch);
140
+ ctx.broadcastTableChange(table, 'MODIFY', key);
141
+ return {};
142
+ }
143
+
144
+ // entityDb.delete(<Table>.ById): a missing record is RECORD_NOT_FOUND with
145
+ // FIELD '' (recorded). VALIDATE acks without a lookup.
146
+ function remove(
147
+ table: string,
148
+ _def: TableDef,
149
+ details: Row,
150
+ ctx: EventHandlerCtx,
151
+ ): EventHandlerResult {
152
+ if (ctx.validate) return {};
153
+ const { store } = ctx;
154
+ const key = store.rowKey(table, details);
155
+ if (!store.getRow(table, key)) throw NackError.recordNotFound(table);
156
+ store.deleteRow(table, key);
157
+ ctx.broadcastTableChange(table, 'DELETE', key);
158
+ return {};
159
+ }
160
+
161
+ const VERBS: Record<
162
+ CrudEventVerb,
163
+ (table: string, def: TableDef, details: Row, ctx: EventHandlerCtx) => EventHandlerResult
164
+ > = { INSERT: insert, MODIFY: modify, DELETE: remove };
165
+
166
+ // The generic handler for an event a table registers through
167
+ // TableDef.events, or undefined. It reads the table's definition on every
168
+ // call, so setTable() takes effect straight away.
169
+ export function crudEventHandler(
170
+ eventName: string,
171
+ config: MockServerConfig,
172
+ ): EventHandler | undefined {
173
+ const crud = crudEventOf(eventName, config);
174
+ if (!crud) return undefined;
175
+ const { table, verb } = crud;
176
+ return (details, ctx) => {
177
+ const def = config.tables?.[table];
178
+ if (!def) throw new Error(`Unknown table: ${table}`);
179
+ return VERBS[verb](table, def, details, ctx);
180
+ };
181
+ }
@@ -1,4 +1,6 @@
1
- import { compileCriteria, filterByCriteria } from '../db/criteria.ts';
1
+ import { compileCriteria, criteriaFields, filterByCriteria } from '../db/criteria.ts';
2
+ import { rowComparator } from '../db/ordering.ts';
3
+ import { queryFieldNames, querySchema } from '../db/schema.ts';
2
4
  import type { Store } from '../db/store.ts';
3
5
  import type { Connection, Subscription } from '../protocol/connection.ts';
4
6
  import { isLegacyFidelity } from '../protocol/fidelity.ts';
@@ -8,9 +10,10 @@ import {
8
10
  resourceNotMappedNack,
9
11
  subscriptionAlreadyExistsNack,
10
12
  } from '../protocol/msgNack.ts';
11
- import { stampRow } from '../protocol/rowUpdate.ts';
12
- import type { GenesisMessage, MockServerConfig, Row } from '../types.ts';
13
- import { applyResourceAuth, requestingUser } from './resourceAuth.ts';
13
+ import { dataserverRow, rowRefFor, stampRow } from '../protocol/rowUpdate.ts';
14
+
15
+ import type { GenesisMessage, IndexDef, MockServerConfig, QueryDef, Row } from '../types.ts';
16
+ import { permittedQueryRow, requestingUser } from './resourceAuth.ts';
14
17
 
15
18
  // The real dataserver's defaults (genesis-pal-dataserver ClientOptions,
16
19
  // GSF 8.15.x) — also what foundation-comms' DatasourceDefaults send.
@@ -75,6 +78,43 @@ function takePage(subscription: Subscription): Row[] {
75
78
  return page;
76
79
  }
77
80
 
81
+ // The dataserver's own refusal of a logon (genesis-pal-dataserver): a
82
+ // LOGON_NACK with a StandardError, sent instead of LOGON_ACK. No subscription
83
+ // is opened, so a later DATA_LOGOFF on it gets the router's 404.
84
+ function logonNack(code: 'INVALID_CRITERIA' | 'INVALID_INDEX', text: string): Row {
85
+ return {
86
+ MESSAGE_TYPE: MESSAGE_TYPE.LOGON_NACK,
87
+ ERROR: [{ '@type': 'StandardError', CODE: code, TEXT: text, STATUS_CODE: '400 Bad Request' }],
88
+ WARNING: [],
89
+ };
90
+ }
91
+
92
+ // DETAILS.FIELDS: a space-separated projection of the query's columns, in the
93
+ // query's order. Names it doesn't expose are dropped, as the real dataserver
94
+ // drops them.
95
+ function requestedFieldNames(
96
+ details: Row,
97
+ query: QueryDef,
98
+ exposed: string[] | undefined,
99
+ config: MockServerConfig,
100
+ store: Store,
101
+ ): string[] | undefined {
102
+ const requested =
103
+ typeof details.FIELDS === 'string' && details.FIELDS.trim() !== ''
104
+ ? details.FIELDS.trim().split(/\s+/)
105
+ : undefined;
106
+ if (!requested) return exposed;
107
+ const available = exposed ?? querySchema(query, config, store).fields.map((field) => field.name);
108
+ return available.filter((name) => requested.includes(name));
109
+ }
110
+
111
+ // Ascending by the index's fields, nulls first. Ties are broken by RECORD_ID,
112
+ // as the real index breaks them, so REVERSE, which flips the whole order,
113
+ // puts the newest of equal rows first.
114
+ function indexComparator(index: IndexDef): (a: Row, b: Row) => number {
115
+ return rowComparator(index.fields.map((field) => ({ field, descending: false })));
116
+ }
117
+
78
118
  // DATA_LOGON opens a persistent subscription keyed by SOURCE_REF: reply with
79
119
  // LOGON_ACK and the first page of the snapshot, then keep the SOURCE_REF
80
120
  // registered so MORE_ROWS can page the rest and later inserts/amends/deletes
@@ -116,6 +156,44 @@ export function handleDataLogon(
116
156
 
117
157
  const criteria = details.CRITERIA_MATCH;
118
158
  const user = requestingUser(connection);
159
+ // The query's fields block (or the source's declared columns) is all a
160
+ // criteria may name and all a row carries; DETAILS.FIELDS narrows the rows.
161
+ // Legacy subscriptions ignore all of it, and ORDER_BY, as 15.47 did.
162
+ const exposed = queryFieldNames(query, config, store);
163
+ let compare: ((a: Row, b: Row) => number) | undefined;
164
+ let reverse = false;
165
+ if (!legacy) {
166
+ // The real dataserver lets a criteria read only the query's own fields
167
+ // (QueryClientFactory.kt). Unknown columns are only checked when the
168
+ // query's fields are known; an inferred query fails open as before.
169
+ const outside = exposed && criteriaFields(criteria)?.find((field) => !exposed.includes(field));
170
+ if (outside) {
171
+ connection.reply(
172
+ message,
173
+ logonNack(
174
+ 'INVALID_CRITERIA',
175
+ `GenericCriteriaValidationException : Criteria validation failed: Variable ${outside} is not allowed in criteria expression`,
176
+ ),
177
+ );
178
+ return;
179
+ }
180
+ // ORDER_BY names one of the query's own indexes (QueryDef.indexes).
181
+ if (details.ORDER_BY) {
182
+ const index = query.indexes?.find((candidate) => candidate.name === details.ORDER_BY);
183
+ if (!index) {
184
+ connection.reply(
185
+ message,
186
+ logonNack(
187
+ 'INVALID_INDEX',
188
+ `No such index ${details.ORDER_BY} for dataserver query ${resourceName}`,
189
+ ),
190
+ );
191
+ return;
192
+ }
193
+ compare = indexComparator(index);
194
+ reverse = details.REVERSE === true;
195
+ }
196
+ }
119
197
  // Order matters: the query's own filter (the where-clause equivalent) and
120
198
  // the user's row permissioning narrow the set BEFORE the client's
121
199
  // CRITERIA_MATCH; hidden fields are stripped last, off the final rows.
@@ -129,20 +207,39 @@ export function handleDataLogon(
129
207
  // the count is taken after it — an assumption, as no recording covers it.
130
208
  const rowsCount = rows.length;
131
209
  rows = filterByCriteria(rows, criteria);
210
+ if (compare) {
211
+ rows = [...rows].sort(compare);
212
+ if (reverse) rows.reverse();
213
+ }
132
214
  // Permissioning is applied per row so the ROW_REF can still be computed from
133
215
  // the UNSTRIPPED row — pk fields may themselves be hidden from this user,
134
216
  // and a snapshot row with no ROW_REF can never be matched to the later
135
217
  // INSERT/MODIFY/DELETE pushes (which stamp the real ref, see server.ts).
218
+ // RECORD_ID (or, in legacy mode, the pk fields) is never sent unless the
219
+ // query's fields block names it.
220
+ const getRowRef = (row: Row) => rowRefFor(config, store, query.source, row);
221
+
222
+ const fieldNames = legacy
223
+ ? undefined
224
+ : requestedFieldNames(details, query, exposed, config, store);
136
225
  const stampedRows: Row[] = [];
226
+ // Each sent-or-waiting row as stored, by pending key, for ORDER_BY's live
227
+ // INSERT placement below.
228
+ const rawRows = new Map<string, Row>();
137
229
  for (const row of rows) {
138
- const [permitted] = applyResourceAuth([row], query.auth, user);
230
+ const permitted = permittedQueryRow(row, query.auth, user, fieldNames);
139
231
  if (!permitted) continue;
140
- stampedRows.push(stampRow(permitted, store.getRowRef(query.source, row), 'INSERT'));
232
+ const rowRef = getRowRef(row);
233
+ const key = pendingKey(rowRef);
234
+ if (key !== undefined) rawRows.set(key, row);
235
+ stampedRows.push(
236
+ stampRow(dataserverRow(permitted, row, query.fields), rowRef, 'INSERT', legacy),
237
+ );
141
238
  }
142
239
 
143
240
  const subscription: Subscription = {
144
241
  resourceName,
145
- getRowRef: (row) => store.getRowRef(query.source, row),
242
+ getRowRef,
146
243
  predicate: compileCriteria(criteria),
147
244
  sequenceId: 1,
148
245
  };
@@ -170,7 +267,30 @@ export function handleDataLogon(
170
267
  held: new Set<string>(),
171
268
  maxRows,
172
269
  maxView,
270
+ fieldNames,
173
271
  };
272
+ // Under ORDER_BY a live INSERT that sorts among the rows still waiting for
273
+ // MORE_ROWS joins them in order rather than being pushed out of order; one
274
+ // that sorts before them lands in what the client has, and is pushed. (A
275
+ // MODIFY of a waiting row keeps its place.)
276
+ if (compare) {
277
+ const ascending = compare;
278
+ const sortCompare = reverse ? (a: Row, b: Row) => ascending(b, a) : ascending;
279
+ tracked.queueInsert = (key, stamped, row) => {
280
+ const waiting = [...tracked.pending!];
281
+ const first = waiting[0] && rawRows.get(waiting[0][0]);
282
+ if (!first || sortCompare(row, first) < 0) return false;
283
+ const at = waiting.findIndex(([waitingKey]) => {
284
+ const other = rawRows.get(waitingKey);
285
+ return other !== undefined && sortCompare(row, other) < 0;
286
+ });
287
+ waiting.splice(at < 0 ? waiting.length : at, 0, [key, stamped]);
288
+ tracked.pending!.clear();
289
+ for (const [waitingKey, waitingRow] of waiting) tracked.pending!.set(waitingKey, waitingRow);
290
+ rawRows.set(key, row);
291
+ return true;
292
+ };
293
+ }
174
294
  const firstPage = takePage(tracked);
175
295
  const pending = tracked.pending!;
176
296
  connection.subscribe(sourceRef, tracked);
@@ -0,0 +1,43 @@
1
+ import { eventFields, isDeclared, resolveEventTarget } from '../db/schema.ts';
2
+ import type { Store } from '../db/store.ts';
3
+ import type { Connection } from '../protocol/connection.ts';
4
+ import type { NackErrorInput } from '../protocol/errors.ts';
5
+ import { schemaFieldErrors, validateSchema } from '../protocol/schemaValidation.ts';
6
+ import type { GenesisMessage, MockServerConfig } from '../types.ts';
7
+ import { eventInboundSchema } from './jsonSchema.ts';
8
+
9
+ // The schema check every event goes through on the real server before its
10
+ // handler runs (GenesisMessageDecoder, GSF v8.15.29): the whole message
11
+ // against the event's INBOUND JSON schema — the one JSON_SCHEMA_REQUEST
12
+ // serves — each failure a FieldError VALIDATION_ERROR (see
13
+ // protocol/schemaValidation.ts). An empty list means it passed.
14
+ //
15
+ // Only an event whose DETAILS class is declared is checked: a table event on
16
+ // a table with TableDef.fields, or an eventSchemas entry with `fields`. An
17
+ // inferred schema is a guess from the seed rows (a column every row leaves
18
+ // empty isn't in it at all), and checking against it would NACK payloads the
19
+ // real server accepts.
20
+ export function validateEventMessage(
21
+ message: GenesisMessage,
22
+ connection: Connection,
23
+ config: MockServerConfig,
24
+ store: Store,
25
+ ): NackErrorInput[] {
26
+ const name = message.MESSAGE_TYPE;
27
+ const target = resolveEventTarget(name, config);
28
+ if (!target) return [];
29
+ const declared =
30
+ target.kind === 'dto' ||
31
+ (target.table !== undefined && isDeclared(target.table, config, store));
32
+ if (!declared) return [];
33
+ const inbound = eventInboundSchema(
34
+ name,
35
+ target,
36
+ eventFields(target, config, store),
37
+ config.appName,
38
+ );
39
+ // The router stamps the session's user on everything it forwards, so the
40
+ // schema's required USER_NAME is always there by the time it is checked.
41
+ const forwarded = { ...message, USER_NAME: connection.userName ?? '' };
42
+ return schemaFieldErrors(validateSchema(inbound, forwarded));
43
+ }