inibase 3.3.0 → 3.5.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/README.md CHANGED
@@ -206,7 +206,7 @@ interface {
206
206
 
207
207
  ```ts
208
208
  interface Field {
209
- id: number; // stored as a Number but displayed as a hashed ID
209
+ id: number; // position in the schema; 0 = the row id, -1 = createdAt, -2 = updatedAt
210
210
  key: string;
211
211
  required?: boolean;
212
212
  unique?: boolean | string; // boolean for simple uniqueness, string for grouped uniqueness
@@ -402,6 +402,30 @@ const userTableSchema = [
402
402
  await db.createTable("user", userTableSchema, userTableConfig);
403
403
  ```
404
404
 
405
+ #### `decodeID`
406
+
407
+ By default a row's `id` is stored as a number but only ever handed back as an
408
+ encrypted (`aes-256-cbc`, hex) string, so ids can't be guessed by incrementing.
409
+ Set `decodeID: true` and the id is exposed as the **plain number** everywhere —
410
+ `get` rows, `get`/`put`/`delete` by id, relation lookups, and the id returned by
411
+ `post`:
412
+
413
+ ```js
414
+ const db = new Inibase("/databaseName");
415
+ await db.createTable("item", [{ key: "name", type: "string" }], {
416
+ decodeID: true,
417
+ });
418
+
419
+ const id = await db.post("item", { name: "widget" }); // => 1 (a number)
420
+ await db.get("item", 1); // works with the raw number
421
+ ```
422
+
423
+ So `post` returns a `number` on a `decodeID` table and an encrypted `string` on
424
+ every other table — in TypeScript its return type is `string | number`
425
+ (`(string | number)[]` for a bulk post). Encrypted ids stay valid inputs on a
426
+ `decodeID` table too, so existing clients that round-trip an id keep working
427
+ either way. `decodeID` can be toggled later with `updateTable`.
428
+
405
429
  </blockquote>
406
430
  </details>
407
431
 
package/dist/file.js CHANGED
@@ -377,7 +377,33 @@ export const isExists = async (path) => {
377
377
  }
378
378
  };
379
379
  /**
380
- * Secures input by encoding/escaping characters.
380
+ * Marks a cell written with the lossless string escaping below. It is a
381
+ * control character that never starts a legacy cell in practice, is not
382
+ * `[`/`{` (so the value is never mistaken for Inison) and is not numeric.
383
+ */
384
+ const ESCAPED_STRING_PREFIX = "\u0001";
385
+ /**
386
+ * Lossless escaping for strings that the legacy newline-only escaping cannot
387
+ * round-trip: `\` becomes `\\`, CR becomes `\r`, LF becomes `\n`.
388
+ */
389
+ const escapeString = (input) => ESCAPED_STRING_PREFIX +
390
+ input.replace(/[\\\r\n]/g, (char) => char === "\\" ? "\\\\" : char === "\r" ? "\\r" : "\\n");
391
+ /** Reverses {@link escapeString} (input includes the prefix). */
392
+ const unescapeString = (input) => input
393
+ .slice(ESCAPED_STRING_PREFIX.length)
394
+ .replace(/\\([\\rn])/g, (_match, char) => char === "\\" ? "\\" : char === "r" ? "\r" : "\n");
395
+ /**
396
+ * Secures input by encoding/escaping characters so a value fits on one line
397
+ * of a column file and reads back exactly as it was written.
398
+ *
399
+ * - The string is stored as given: it is NOT URI-decoded (`%20`, `50%`,
400
+ * `%D8%A7` are data, not transport encoding).
401
+ * - Strings without `\` or CR keep the legacy format (LF stored as `\n`), so
402
+ * existing files, native whole-line search and most new writes are
403
+ * byte-identical to before.
404
+ * - Strings containing `\` or CR (or starting with the marker) use the
405
+ * lossless format, because in the legacy format a literal `\n` typed by the
406
+ * user is indistinguishable from an escaped line break.
381
407
  *
382
408
  * @param input - String, number, boolean, or null.
383
409
  * @returns Encoded string for true/false, special characters in strings, or original input.
@@ -390,15 +416,11 @@ const secureString = (input) => {
390
416
  return "";
391
417
  return input;
392
418
  }
393
- let decodedInput = null;
394
- try {
395
- decodedInput = decodeURIComponent(input);
396
- }
397
- catch (_error) {
398
- decodedInput = decodeURIComponent(input.replace(/%(?![0-9][0-9a-fA-F]+)/g, ""));
399
- }
400
- // Replace characters using a single regular expression.
401
- return decodedInput.replace(/\r\n|\r|\n/g, "\\n");
419
+ if (input.includes("\\") ||
420
+ input.includes("\r") ||
421
+ input.startsWith(ESCAPED_STRING_PREFIX))
422
+ return escapeString(input);
423
+ return input.replace(/\n/g, "\\n");
402
424
  };
403
425
  /**
404
426
  * Encodes the input using 'secureString' and 'Inison.stringify' functions.
@@ -425,8 +447,10 @@ export const encode = (input) => Array.isArray(input)
425
447
  const unSecureString = (input) => {
426
448
  if (isNumber(input))
427
449
  return String(input).at(0) === "0" ? input : Number(input);
428
- // Fast path: the common case has no `\n` escape sequence, so avoid
429
- // allocating a replacement string (and a fresh RegExp) per cell.
450
+ if (typeof input === "string" && input.startsWith(ESCAPED_STRING_PREFIX))
451
+ return unescapeString(input);
452
+ // Legacy format. Fast path: the common case has no `\n` escape sequence,
453
+ // so avoid allocating a replacement string (and a fresh RegExp) per cell.
430
454
  if (typeof input === "string")
431
455
  return input.includes("\\n")
432
456
  ? input.replaceAll("\\n", "\n") || null
package/dist/index.d.ts CHANGED
@@ -384,9 +384,13 @@ export default class Inibase {
384
384
  * @param {Options} [options] Pagination options, useful when the returnPostedData param is true
385
385
  * @param {boolean} [returnPostedData] By default function returns void, if you want to get the posted data, set this param to true
386
386
  * @return {*} {Promise<Data | Data[] | null | void>}
387
+ *
388
+ * @remarks Without `returnPostedData` the return value is the new id: a
389
+ * `number` on a table created with `decodeID: true`, an encrypted
390
+ * `string` on every other table.
387
391
  */
388
- post<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: Data & TData, options?: Options, returnPostedData?: boolean): Promise<string>;
389
- post<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData)[], options?: Options, returnPostedData?: boolean): Promise<string[]>;
392
+ post<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: Data & TData, options?: Options, returnPostedData?: boolean): Promise<string | number>;
393
+ post<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData)[], options?: Options, returnPostedData?: boolean): Promise<(string | number)[]>;
390
394
  post<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: Data & TData, options: Options | undefined, returnPostedData: true): Promise<(Data & TData) | null>;
391
395
  post<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData)[], options: Options | undefined, returnPostedData: true): Promise<(Data & TData)[] | null>;
392
396
  private _post;
package/dist/index.js CHANGED
@@ -2902,12 +2902,18 @@ export default class Inibase {
2902
2902
  : this.totalItems.get(`${tableName}-*`), options, !Utils.isArrayOfObjects(clonedData), // return only one item if data is not array of objects
2903
2903
  undefined, true);
2904
2904
  }
2905
+ // On a decodeID table the id *is* the plain number — that is the
2906
+ // whole point of the config: get(), put(), delete() and the
2907
+ // returnPostedData read all hand back raw numbers, so post() must
2908
+ // not encode it here. Every other table keeps returning the
2909
+ // encrypted id.
2910
+ const tableConfig = globalConfig[this.databasePath].tables?.get(tableName)?.config;
2911
+ const postedID = (id) => tableConfig?.decodeID === true
2912
+ ? Number(id)
2913
+ : UtilsServer.encodeID(id);
2905
2914
  return Array.isArray(clonedData)
2906
- ? (globalConfig[this.databasePath].tables?.get(tableName)?.config
2907
- .prepend
2908
- ? clonedData.toReversed()
2909
- : clonedData).map(({ id }) => UtilsServer.encodeID(id))
2910
- : UtilsServer.encodeID(clonedData.id);
2915
+ ? (tableConfig?.prepend ? clonedData.toReversed() : clonedData).map(({ id }) => postedID(id))
2916
+ : postedID(clonedData.id);
2911
2917
  }
2912
2918
  finally {
2913
2919
  if (this.transaction) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inibase",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Karim Amahtil",
@@ -88,7 +88,7 @@
88
88
  "benchmark": "./benchmark/run.js",
89
89
  "benchmark:durability": "tsx ./benchmark/durability.ts",
90
90
  "benchmark:computed": "tsx ./benchmark/computed.ts",
91
- "test": "tsx ./tests/inibase.test.ts && tsx ./tests/inibase.advanced.test.ts && tsx ./tests/transaction.test.ts && tsx ./tests/durability.test.ts && tsx ./tests/expression.test.ts && tsx ./tests/utils.test.ts && tsx ./tests/sum.nested.test.ts && tsx ./tests/computed.child.test.ts && tsx ./tests/hardening.test.ts",
91
+ "test": "tsx ./tests/inibase.test.ts && tsx ./tests/inibase.advanced.test.ts && tsx ./tests/transaction.test.ts && tsx ./tests/durability.test.ts && tsx ./tests/expression.test.ts && tsx ./tests/utils.test.ts && tsx ./tests/sum.nested.test.ts && tsx ./tests/computed.child.test.ts && tsx ./tests/hardening.test.ts && tsx ./tests/string.roundtrip.test.ts",
92
92
  "test:durability": "tsx ./tests/durability.test.ts",
93
93
  "test:transaction": "tsx ./tests/transaction.test.ts",
94
94
  "test:utils": "tsx ./tests/utils.test.ts",