@promakeai/orm 1.0.6 → 1.3.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/dist/types.d.ts CHANGED
@@ -25,7 +25,7 @@ export interface FieldReference {
25
25
  onUpdate?: "CASCADE" | "SET_NULL" | "RESTRICT" | "NO_ACTION";
26
26
  }
27
27
  /**
28
- * Complete field definition after DSL processing
28
+ * Complete field definition (normalized from JSON schema)
29
29
  */
30
30
  export interface FieldDefinition {
31
31
  type: FieldType;
@@ -47,12 +47,27 @@ export interface FieldDefinition {
47
47
  enum?: string[];
48
48
  match?: string;
49
49
  }
50
+ /**
51
+ * Permission roles for table access control
52
+ */
53
+ export type PermissionRole = "anon" | "user" | "admin";
54
+ /**
55
+ * Permission actions
56
+ */
57
+ export type PermissionAction = "create" | "read" | "update" | "delete";
58
+ /**
59
+ * Table-level permissions mapping roles to allowed actions.
60
+ * If not defined on a table, no restrictions apply (backward compatible).
61
+ * Enforced by the backend, not by the ORM.
62
+ */
63
+ export type TablePermissions = Partial<Record<PermissionRole, PermissionAction[]>>;
50
64
  /**
51
65
  * Table definition with all fields
52
66
  */
53
67
  export interface TableDefinition {
54
68
  name: string;
55
69
  fields: Record<string, FieldDefinition>;
70
+ permissions?: TablePermissions;
56
71
  }
57
72
  /**
58
73
  * Language configuration
@@ -69,20 +84,6 @@ export interface SchemaDefinition {
69
84
  languages: LanguageConfig;
70
85
  tables: Record<string, TableDefinition>;
71
86
  }
72
- /**
73
- * Interface for FieldBuilder-like objects (for type checking)
74
- */
75
- export interface FieldBuilderLike {
76
- build(): FieldDefinition;
77
- }
78
- /**
79
- * Raw schema input before processing (from user DSL)
80
- */
81
- export interface SchemaInput {
82
- name?: string;
83
- languages: string[] | LanguageConfig;
84
- tables: Record<string, Record<string, FieldBuilderLike>>;
85
- }
86
87
  /**
87
88
  * JSON-native type syntax for schema.json
88
89
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promakeai/orm",
3
- "version": "1.0.6",
3
+ "version": "1.3.0",
4
4
  "description": "Database-agnostic ORM core - works in browser and Node.js",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,483 @@
1
+ /**
2
+ * REST API Adapter
3
+ *
4
+ * Communicates with a backend REST API implementing the /database endpoints.
5
+ * Works in both Node.js and browser environments (uses fetch).
6
+ */
7
+
8
+ import type { IDataAdapter } from "./IDataAdapter";
9
+ import type { QueryOptions, PaginatedResult, SchemaDefinition } from "../types";
10
+ import { toTranslationTableName, toTranslationFKName } from "../schema";
11
+
12
+ type FetchFn = (input: string, init?: any) => Promise<any>;
13
+
14
+ export interface RestAdapterConfig {
15
+ /** Base URL for API (e.g., "https://backend.promake.ai/api") */
16
+ baseUrl: string;
17
+ /** Database endpoint prefix (default: "/database") */
18
+ databasePrefix?: string;
19
+ /** Static authorization token for Bearer auth */
20
+ token?: string;
21
+ /** Dynamic token getter - called on every request. Takes priority over static token. */
22
+ getToken?: () => string | null;
23
+ /** Custom headers to include in all requests */
24
+ headers?: Record<string, string>;
25
+ /** Schema definition for translation support */
26
+ schema?: SchemaDefinition;
27
+ /** Default language for translations */
28
+ defaultLang?: string;
29
+ /** Custom fetch function (for testing/SSR) */
30
+ fetch?: FetchFn;
31
+ /** Request timeout in ms (default: 30000) */
32
+ timeout?: number;
33
+ }
34
+
35
+ export class RestAdapter implements IDataAdapter {
36
+ private baseUrl: string;
37
+ private databasePrefix: string;
38
+ private token?: string;
39
+ private getTokenFn?: () => string | null;
40
+ private customHeaders: Record<string, string>;
41
+ private fetchFn: FetchFn;
42
+ private timeout: number;
43
+
44
+ schema?: SchemaDefinition;
45
+ defaultLang?: string;
46
+
47
+ constructor(config: RestAdapterConfig) {
48
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
49
+ this.databasePrefix = config.databasePrefix ?? "/database";
50
+ this.token = config.token;
51
+ this.getTokenFn = config.getToken;
52
+ this.customHeaders = config.headers ?? {};
53
+ const gf = globalThis as any;
54
+ this.fetchFn = config.fetch ?? gf.fetch.bind(gf);
55
+ this.timeout = config.timeout ?? 30000;
56
+ this.schema = config.schema;
57
+ this.defaultLang = config.defaultLang;
58
+ }
59
+
60
+ // ==================== Private Helpers ====================
61
+
62
+ private buildUrl(path: string): string {
63
+ return `${this.baseUrl}${this.databasePrefix}${path}`;
64
+ }
65
+
66
+ private getHeaders(): Record<string, string> {
67
+ const headers: Record<string, string> = {
68
+ "Content-Type": "application/json",
69
+ Accept: "application/json",
70
+ ...this.customHeaders,
71
+ };
72
+ const token = this.getTokenFn ? this.getTokenFn() : this.token;
73
+ if (token) {
74
+ headers["Authorization"] = `Bearer ${token}`;
75
+ }
76
+ return headers;
77
+ }
78
+
79
+ private async request<T>(url: string, init?: Record<string, any>): Promise<T> {
80
+ const controller = new AbortController();
81
+ const timer = setTimeout(() => controller.abort(), this.timeout);
82
+
83
+ try {
84
+ const response: any = await this.fetchFn(url, {
85
+ ...init,
86
+ headers: { ...this.getHeaders(), ...(init?.headers ?? {}) },
87
+ signal: controller.signal,
88
+ });
89
+
90
+ if (!response.ok) {
91
+ const body = await response.json().catch(() => null);
92
+ const message =
93
+ body?.message || body?.error || `HTTP ${response.status} ${response.statusText}`;
94
+ throw new Error(message);
95
+ }
96
+
97
+ return await response.json();
98
+ } finally {
99
+ clearTimeout(timer);
100
+ }
101
+ }
102
+
103
+ private buildQueryParams(options?: QueryOptions): URLSearchParams {
104
+ const params = new URLSearchParams();
105
+ if (!options) return params;
106
+
107
+ if (options.where && Object.keys(options.where).length > 0) {
108
+ params.set("where", JSON.stringify(options.where));
109
+ }
110
+ if (options.orderBy?.length) {
111
+ params.set(
112
+ "order",
113
+ options.orderBy.map((o) => `${o.field}:${o.direction}`).join(",")
114
+ );
115
+ }
116
+ if (options.limit != null) {
117
+ params.set("limit", String(options.limit));
118
+ }
119
+ if (options.offset != null) {
120
+ params.set("offset", String(options.offset));
121
+ }
122
+ if (options.lang) {
123
+ params.set("lang", options.lang);
124
+ }
125
+ if (options.fallbackLang) {
126
+ params.set("fallback_lang", options.fallbackLang);
127
+ }
128
+
129
+ return params;
130
+ }
131
+
132
+ private urlWithParams(base: string, params: URLSearchParams): string {
133
+ const qs = params.toString();
134
+ return qs ? `${base}?${qs}` : base;
135
+ }
136
+
137
+ // ==================== Lifecycle ====================
138
+
139
+ setSchema(schema: SchemaDefinition): void {
140
+ this.schema = schema;
141
+ }
142
+
143
+ async connect(): Promise<void> {
144
+ if (!this.schema) return;
145
+
146
+ // Convert internal SchemaDefinition to the API's expected format
147
+ const apiSchema: Record<string, { columns: Record<string, unknown>[] }> = {};
148
+
149
+ for (const [tableName, tableDef] of Object.entries(this.schema.tables)) {
150
+ const columns: Record<string, unknown>[] = [];
151
+ for (const [fieldName, fieldDef] of Object.entries(tableDef.fields)) {
152
+ const col: Record<string, unknown> = {
153
+ name: fieldName,
154
+ type: this.mapFieldType(fieldDef.type),
155
+ };
156
+ if (fieldDef.primary) col.primary_key = true;
157
+ if (!fieldDef.nullable) col.not_null = true;
158
+ if (fieldDef.unique) col.unique = true;
159
+ if (fieldDef.default !== undefined) col.default_value = String(fieldDef.default);
160
+ if (fieldDef.translatable) col.translatable = true;
161
+ columns.push(col);
162
+ }
163
+ const tableEntry: Record<string, unknown> = { columns };
164
+ if (tableDef.permissions) {
165
+ tableEntry.permissions = tableDef.permissions;
166
+ }
167
+ apiSchema[tableName] = tableEntry as { columns: Record<string, unknown>[] };
168
+ }
169
+
170
+ await this.request(this.buildUrl("/generate"), {
171
+ method: "POST",
172
+ body: JSON.stringify({ schema: { tables: apiSchema } }),
173
+ });
174
+ }
175
+
176
+ private mapFieldType(type: string): string {
177
+ switch (type) {
178
+ case "id":
179
+ case "int":
180
+ return "INTEGER";
181
+ case "string":
182
+ case "text":
183
+ case "timestamp":
184
+ return "TEXT";
185
+ case "decimal":
186
+ return "REAL";
187
+ case "bool":
188
+ return "INTEGER";
189
+ case "json":
190
+ case "object":
191
+ case "object[]":
192
+ case "string[]":
193
+ case "number[]":
194
+ case "boolean[]":
195
+ return "TEXT";
196
+ default:
197
+ return "TEXT";
198
+ }
199
+ }
200
+
201
+ close(): void {
202
+ // No-op for REST
203
+ }
204
+
205
+ // ==================== Query Methods ====================
206
+
207
+ async list<T = unknown>(table: string, options?: QueryOptions): Promise<T[]> {
208
+ const params = this.buildQueryParams(options);
209
+ const url = this.urlWithParams(this.buildUrl(`/${table}/list`), params);
210
+ const res = await this.request<{ data: T[] }>(url);
211
+ return res.data;
212
+ }
213
+
214
+ async get<T = unknown>(
215
+ table: string,
216
+ id: string | number,
217
+ options?: QueryOptions
218
+ ): Promise<T | null> {
219
+ const params = new URLSearchParams();
220
+ if (options?.lang) params.set("lang", options.lang);
221
+ if (options?.fallbackLang) params.set("fallback_lang", options.fallbackLang);
222
+ const url = this.urlWithParams(this.buildUrl(`/${table}/${id}`), params);
223
+
224
+ try {
225
+ const res = await this.request<{ data: T }>(url);
226
+ return res.data ?? null;
227
+ } catch (err) {
228
+ if (err instanceof Error && err.message.includes("404")) {
229
+ return null;
230
+ }
231
+ throw err;
232
+ }
233
+ }
234
+
235
+ async findOne<T = unknown>(
236
+ table: string,
237
+ options?: QueryOptions
238
+ ): Promise<T | null> {
239
+ const results = await this.list<T>(table, { ...options, limit: 1 });
240
+ return results[0] ?? null;
241
+ }
242
+
243
+ async count(table: string, options?: QueryOptions): Promise<number> {
244
+ const params = new URLSearchParams();
245
+ if (options?.where && Object.keys(options.where).length > 0) {
246
+ params.set("where", JSON.stringify(options.where));
247
+ }
248
+ const url = this.urlWithParams(this.buildUrl(`/${table}/count`), params);
249
+ const res = await this.request<{ count: number }>(url);
250
+ return res.count;
251
+ }
252
+
253
+ async paginate<T = unknown>(
254
+ table: string,
255
+ page: number,
256
+ limit: number,
257
+ options?: QueryOptions
258
+ ): Promise<PaginatedResult<T>> {
259
+ const offset = (page - 1) * limit;
260
+ const [total, data] = await Promise.all([
261
+ this.count(table, options),
262
+ this.list<T>(table, { ...options, limit, offset }),
263
+ ]);
264
+ const totalPages = Math.ceil(total / limit);
265
+ return {
266
+ data,
267
+ page,
268
+ limit,
269
+ total,
270
+ totalPages,
271
+ hasMore: page < totalPages,
272
+ };
273
+ }
274
+
275
+ // ==================== Write Methods ====================
276
+
277
+ async create<T = unknown>(
278
+ table: string,
279
+ data: Record<string, unknown>
280
+ ): Promise<T> {
281
+ const url = this.buildUrl(`/${table}/create`);
282
+ const res = await this.request<{ data: T; id: unknown }>(url, {
283
+ method: "POST",
284
+ body: JSON.stringify({ data }),
285
+ });
286
+ // Backend returns id at top level, merge it into data
287
+ const result = (res.data ?? {}) as Record<string, unknown>;
288
+ if (res.id !== undefined && !("id" in result)) {
289
+ result.id = res.id;
290
+ }
291
+ return result as T;
292
+ }
293
+
294
+ async update<T = unknown>(
295
+ table: string,
296
+ id: string | number,
297
+ data: Record<string, unknown>,
298
+ options?: { translations?: Record<string, Record<string, unknown>> }
299
+ ): Promise<T> {
300
+ const url = this.buildUrl(`/${table}/${id}`);
301
+ const body: Record<string, unknown> = { data };
302
+ if (options?.translations) {
303
+ body.translations = options.translations;
304
+ }
305
+ const res = await this.request<{ data: T }>(url, {
306
+ method: "PUT",
307
+ body: JSON.stringify(body),
308
+ });
309
+ return res.data;
310
+ }
311
+
312
+ async delete(table: string, id: string | number): Promise<boolean> {
313
+ const url = this.buildUrl(`/${table}/${id}`);
314
+ try {
315
+ const res = await this.request<{ deleted: boolean }>(url, {
316
+ method: "DELETE",
317
+ });
318
+ return res.deleted ?? true;
319
+ } catch (err) {
320
+ if (err instanceof Error && err.message.includes("404")) {
321
+ return false;
322
+ }
323
+ throw err;
324
+ }
325
+ }
326
+
327
+ // ==================== Batch Methods ====================
328
+
329
+ async createMany<T = unknown>(
330
+ table: string,
331
+ records: Record<string, unknown>[],
332
+ options?: { ignore?: boolean; noAtomic?: boolean }
333
+ ): Promise<{ created: number; ids: (number | bigint)[] }> {
334
+ const url = this.buildUrl(`/${table}/create-batch`);
335
+ const res = await this.request<{ created: number; ids: (number | bigint)[] }>(url, {
336
+ method: "POST",
337
+ body: JSON.stringify({
338
+ records,
339
+ ignore: options?.ignore ?? false,
340
+ no_atomic: options?.noAtomic ?? false,
341
+ }),
342
+ });
343
+ return { created: res.created, ids: res.ids };
344
+ }
345
+
346
+ async updateMany(
347
+ table: string,
348
+ updates: { id: number | string; data: Record<string, unknown> }[],
349
+ options?: { noAtomic?: boolean }
350
+ ): Promise<{ updated: number }> {
351
+ const url = this.buildUrl(`/${table}/update-batch`);
352
+ const res = await this.request<{ updated: number }>(url, {
353
+ method: "POST",
354
+ body: JSON.stringify({
355
+ updates,
356
+ no_atomic: options?.noAtomic ?? false,
357
+ }),
358
+ });
359
+ return { updated: res.updated };
360
+ }
361
+
362
+ async deleteMany(
363
+ table: string,
364
+ ids: (number | string)[],
365
+ options?: { noAtomic?: boolean }
366
+ ): Promise<{ deleted: number }> {
367
+ const url = this.buildUrl(`/${table}/delete-batch`);
368
+ const res = await this.request<{ deleted: number }>(url, {
369
+ method: "POST",
370
+ body: JSON.stringify({
371
+ ids,
372
+ no_atomic: options?.noAtomic ?? false,
373
+ }),
374
+ });
375
+ return { deleted: res.deleted };
376
+ }
377
+
378
+ // ==================== Translation Methods ====================
379
+
380
+ async createWithTranslations<T = unknown>(
381
+ table: string,
382
+ data: Record<string, unknown>,
383
+ translations?: Record<string, Record<string, unknown>>
384
+ ): Promise<T> {
385
+ const url = this.buildUrl(`/${table}/create`);
386
+ const res = await this.request<{ data: T; id: unknown }>(url, {
387
+ method: "POST",
388
+ body: JSON.stringify({ data, translations }),
389
+ });
390
+ // Backend returns id at top level, merge it into data
391
+ const result = (res.data ?? {}) as Record<string, unknown>;
392
+ if (res.id !== undefined && !("id" in result)) {
393
+ result.id = res.id;
394
+ }
395
+ return result as T;
396
+ }
397
+
398
+ async upsertTranslation(
399
+ table: string,
400
+ id: string | number,
401
+ lang: string,
402
+ data: Record<string, unknown>
403
+ ): Promise<void> {
404
+ const url = this.buildUrl(`/${table}/${id}`);
405
+ await this.request(url, {
406
+ method: "PUT",
407
+ body: JSON.stringify({ data, lang }),
408
+ });
409
+ }
410
+
411
+ async getTranslations<T = unknown>(
412
+ table: string,
413
+ id: string | number
414
+ ): Promise<T[]> {
415
+ const translationTable = toTranslationTableName(table);
416
+ const fkName = toTranslationFKName(table);
417
+ const params = new URLSearchParams();
418
+ params.set("where", JSON.stringify({ [fkName]: id }));
419
+ const url = this.urlWithParams(
420
+ this.buildUrl(`/${translationTable}/list`),
421
+ params
422
+ );
423
+ const res = await this.request<{ data: T[] }>(url);
424
+ return res.data;
425
+ }
426
+
427
+ // ==================== Raw Query Methods ====================
428
+
429
+ async raw<T = unknown>(query: string, params?: unknown[]): Promise<T[]> {
430
+ const url = this.buildUrl("/exec");
431
+ const res = await this.request<{ data: T[] }>(url, {
432
+ method: "POST",
433
+ body: JSON.stringify({ sql: query, params: params ?? [] }),
434
+ });
435
+ return res.data;
436
+ }
437
+
438
+ async execute(
439
+ query: string,
440
+ params?: unknown[]
441
+ ): Promise<{ changes: number; lastInsertRowid: number | bigint }> {
442
+ const url = this.buildUrl("/exec");
443
+ const res = await this.request<{
444
+ changes: number;
445
+ last_insert_rowid: number;
446
+ }>(url, {
447
+ method: "POST",
448
+ body: JSON.stringify({ sql: query, params: params ?? [] }),
449
+ });
450
+ return {
451
+ changes: res.changes,
452
+ lastInsertRowid: res.last_insert_rowid ?? 0,
453
+ };
454
+ }
455
+
456
+ // ==================== Transaction Methods ====================
457
+
458
+ async beginTransaction(): Promise<void> {
459
+ // No-op: REST API handles atomicity per-request
460
+ }
461
+
462
+ async commit(): Promise<void> {
463
+ // No-op
464
+ }
465
+
466
+ async rollback(): Promise<void> {
467
+ // No-op
468
+ }
469
+
470
+ // ==================== Schema Methods ====================
471
+
472
+ async getTables(): Promise<string[]> {
473
+ const url = this.buildUrl("/tables");
474
+ const res = await this.request<{ tables: string[] }>(url);
475
+ return res.tables;
476
+ }
477
+
478
+ async getTableSchema(table: string): Promise<unknown[]> {
479
+ const url = this.buildUrl(`/${table}/schema`);
480
+ const res = await this.request<{ columns: unknown[] }>(url);
481
+ return res.columns;
482
+ }
483
+ }
package/src/index.ts CHANGED
@@ -5,21 +5,21 @@
5
5
  *
6
6
  * @example
7
7
  * ```typescript
8
- * import { ORM, defineSchema, f } from '@promakeai/orm';
8
+ * import { ORM, parseJSONSchema } from '@promakeai/orm';
9
9
  *
10
- * // Define schema
11
- * const schema = defineSchema({
10
+ * // Parse JSON schema
11
+ * const schema = parseJSONSchema({
12
12
  * languages: ['en', 'tr'],
13
13
  * tables: {
14
14
  * products: {
15
- * id: f.id(),
16
- * name: f.string().translatable().required(),
17
- * price: f.decimal().required(),
18
- * categoryId: f.int().ref('categories'),
15
+ * id: { type: 'id' },
16
+ * name: { type: 'string', translatable: true, required: true },
17
+ * price: { type: 'decimal', required: true },
18
+ * categoryId: { type: 'int', ref: 'categories' },
19
19
  * },
20
20
  * categories: {
21
- * id: f.id(),
22
- * name: f.string().translatable().required(),
21
+ * id: { type: 'id' },
22
+ * name: { type: 'string', translatable: true, required: true },
23
23
  * },
24
24
  * },
25
25
  * });
@@ -39,14 +39,8 @@ export { ORM } from "./ORM";
39
39
  // Adapter Interface
40
40
  export type { IDataAdapter } from "./adapters/IDataAdapter";
41
41
 
42
- // Schema API
43
- export {
44
- defineSchema,
45
- f,
46
- mergeSchemas,
47
- createSchemaUnsafe,
48
- } from "./schema";
49
- export type { FieldBuilder } from "./schema";
42
+ // REST Adapter
43
+ export { RestAdapter, type RestAdapterConfig } from "./adapters/RestAdapter";
50
44
 
51
45
  // Schema Validation
52
46
  export {
@@ -86,6 +80,7 @@ export {
86
80
  getRequiredFields,
87
81
  getRefTarget,
88
82
  getRefTargetFull,
83
+ getTablePermissions,
89
84
  } from "./schema";
90
85
 
91
86
  // Query Builder
@@ -113,12 +108,12 @@ export {
113
108
  } from "./utils/populateResolver";
114
109
  export type { PopulateAdapter } from "./utils/populateResolver";
115
110
 
116
- // JSON Schema Converter
117
- export {
118
- parseJSONSchema,
119
- parseJSONSchemaWithWarnings,
120
- } from "./utils/jsonConverter";
121
- export type { ParseJSONSchemaResult } from "./utils/jsonConverter";
111
+ // JSON Schema Converter
112
+ export {
113
+ parseJSONSchema,
114
+ parseJSONSchemaWithWarnings,
115
+ } from "./utils/jsonConverter";
116
+ export type { ParseJSONSchemaResult } from "./utils/jsonConverter";
122
117
 
123
118
  // Type Helpers
124
119
  export { isJsonType } from "./types";
@@ -132,13 +127,15 @@ export type {
132
127
  FieldType,
133
128
  FieldDefinition,
134
129
  FieldReference,
135
- FieldBuilderLike,
136
-
137
130
  // Schema types
138
131
  TableDefinition,
139
132
  LanguageConfig,
140
133
  SchemaDefinition,
141
- SchemaInput,
134
+
135
+ // Permission types
136
+ PermissionRole,
137
+ PermissionAction,
138
+ TablePermissions,
142
139
 
143
140
  // JSON Schema types (AI-friendly)
144
141
  JSONFieldType,
@@ -2,10 +2,6 @@
2
2
  * Schema Module Exports
3
3
  */
4
4
 
5
- // Main API
6
- export { defineSchema, f, mergeSchemas, createSchemaUnsafe } from "./defineSchema";
7
- export type { FieldBuilder } from "./fieldBuilder";
8
-
9
5
  // Validation
10
6
  export {
11
7
  validateSchema,
@@ -44,4 +40,5 @@ export {
44
40
  getRequiredFields,
45
41
  getRefTarget,
46
42
  getRefTargetFull,
43
+ getTablePermissions,
47
44
  } from "./schemaHelpers";
@@ -4,7 +4,7 @@
4
4
  * Utilities for working with schema definitions.
5
5
  */
6
6
 
7
- import type { TableDefinition, FieldDefinition, FieldReference } from "../types";
7
+ import type { TableDefinition, FieldDefinition, FieldReference, TablePermissions } from "../types";
8
8
  import { isJsonType } from "../types";
9
9
 
10
10
  /**
@@ -122,3 +122,10 @@ export function getRefTargetFull(
122
122
  }
123
123
  return { table: field.ref.table, field: field.ref.field || "id" };
124
124
  }
125
+
126
+ /**
127
+ * Get table permissions (undefined if no restrictions)
128
+ */
129
+ export function getTablePermissions(table: TableDefinition): TablePermissions | undefined {
130
+ return table.permissions;
131
+ }