ac-storage 0.11.0 → 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.
Files changed (41) hide show
  1. package/README.md +32 -0
  2. package/dist/bundle.cjs +153 -31
  3. package/dist/bundle.cjs.map +1 -1
  4. package/dist/bundle.mjs +153 -31
  5. package/dist/bundle.mjs.map +1 -1
  6. package/dist/index.d.ts +2 -1
  7. package/package.json +2 -2
  8. package/dist/features/StorageAccess/StorageAccess.d.ts +0 -14
  9. package/dist/features/StorageAccess/index.d.ts +0 -3
  10. package/dist/features/StorageAccess/types.d.ts +0 -20
  11. package/dist/features/StorageAccessControl/StorageAccessControl.d.ts +0 -22
  12. package/dist/features/StorageAccessControl/errors.d.ts +0 -13
  13. package/dist/features/StorageAccessControl/index.d.ts +0 -4
  14. package/dist/features/StorageAccessControl/types.d.ts +0 -15
  15. package/dist/features/TreeExplorer/TreeExplorer.d.ts +0 -14
  16. package/dist/features/TreeExplorer/TreeExplorer.test.d.ts +0 -1
  17. package/dist/features/TreeExplorer/index.d.ts +0 -2
  18. package/dist/features/accessors/BinaryAccessor.d.ts +0 -13
  19. package/dist/features/accessors/JSONAccessor/JSONAccessor.d.ts +0 -30
  20. package/dist/features/accessors/JSONAccessor/JSONAccessor.test.d.ts +0 -1
  21. package/dist/features/accessors/JSONAccessor/MemJSONAccessor.d.ts +0 -9
  22. package/dist/features/accessors/JSONAccessor/index.d.ts +0 -2
  23. package/dist/features/accessors/MemBinaryAccessor.d.ts +0 -12
  24. package/dist/features/accessors/MemTextAccessor.d.ts +0 -11
  25. package/dist/features/accessors/TextAccessor.d.ts +0 -12
  26. package/dist/features/accessors/errors.d.ts +0 -3
  27. package/dist/features/accessors/index.d.ts +0 -7
  28. package/dist/features/accessors/types.d.ts +0 -25
  29. package/dist/features/storage/FSStorage.d.ts +0 -28
  30. package/dist/features/storage/IStorage.d.ts +0 -20
  31. package/dist/features/storage/MemStorage.d.ts +0 -7
  32. package/dist/features/storage/errors.d.ts +0 -3
  33. package/dist/features/storage/index.d.ts +0 -4
  34. package/dist/test/entry.test.d.ts +0 -1
  35. package/dist/test/storage-access-control.test.d.ts +0 -1
  36. package/dist/test/storage-accessor.test.d.ts +0 -1
  37. package/dist/test/storage-fs.test.d.ts +0 -1
  38. package/dist/test/storage.test.d.ts +0 -1
  39. package/dist/test-utils/index.d.ts +0 -1
  40. package/dist/types/json.d.ts +0 -13
  41. package/dist/types/storage-access.d.ts +0 -20
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # AC-Storage
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ npm install ac-storage
7
+ ```
8
+
9
+ ## Example
10
+
11
+ ```ts
12
+ import { ACStorage, StorageAccess } from 'ac-storage';
13
+
14
+ const storage = new ACStorage('./store');
15
+ storage.register({
16
+ 'auth' : {
17
+ 'default.json' : StorageAccess.JSON(),
18
+ },
19
+ 'cache' : {
20
+ 'last_access.txt' : StorageAccess.Text(),
21
+ }
22
+ });
23
+
24
+ const authAC = await storage.accessAsJSON('auth:default.json');
25
+ authAC.setOne('id', 'user');
26
+
27
+ const lastAccessAC = await storage.accessAsText('cache:last_access.txt');
28
+ lastAccessAC.write('20250607');
29
+
30
+ await storage.commit();
31
+ ```
32
+
package/dist/bundle.cjs CHANGED
@@ -27,7 +27,7 @@ var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs$1);
27
27
  var fs__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(fs$2);
28
28
  var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
29
29
 
30
- let e$1 = class e extends Error{constructor(e){super(e),this.name="TreeNavigateError";}};const t$1="--tree-leaf";var i$1;let r$1 = class r{#e={};#t=".";#i=false;#r=true;constructor(){}static from(e,t={}){const r=new i$1;return r.#e=e,r.#t=t.delimiter??".",r.#i=t.allowWildcard??false,r.#r=t.allowRecursiveWildcard??true,r}subtree(t){const r=t.split(this.#t),a=this.#a(r,this.#e);if(!a)throw new e$1(`Path '${t}' does not exist`);if(i$1.#l(a.value)||i$1.#s(a.value))throw new e$1(`Path '${t}' is not a subtree`);const l=a.path.reduce(((e,t)=>e[t]),this.#e),s=new i$1;return s.#e=l,s.#t=this.#t,s.#i=this.#i,s}get(e,t){return this.walk(e,t)?.value??null}walk(e,t={}){if(""===e)return t.allowIntermediate?{value:this.#e,path:[]}:null;const r=e.split(this.#t),a=this.#a(r,this.#e);if(!a)return null;const l=a.value;return i$1.#l(l)?a:i$1.#s(l)?{value:l.value,path:a.path}:t.allowIntermediate?a:null}trace(e){const t=e.split(this.#t),r={treePath:[]},a=this.#a(t,this.#e,r);let l,s;if(a){const e=a.value;return i$1.#l(e)?(l=true,s=e):i$1.#s(e)?(l=true,s=e.value):(l=false,s=e),{find:true,isLeaf:l,value:s,nodePath:a?.path??[],tracePath:r.treePath,untracePath:t}}return {find:false,isLeaf:false,value:void 0,nodePath:[],tracePath:r.treePath,untracePath:t}}#a(e,t,r={treePath:[]}){const{treePath:a}=r,l=e.shift();if(null==l)return {value:t,path:[]};if("object"!=typeof t||i$1.#s(t))return e.unshift(l),null;if(a.push(l),l in t){const i=this.#a(e,t[l],r);if(i)return {value:i.value,path:[l,...i.path]}}else if(this.#i){if("*"in t){const i=this.#a(e,t["*"],r);if(i)return {value:i.value,path:["*",...i.path]}}if(this.#r&&"**/*"in t)return {value:t["**/*"],path:["**/*"]}}return null}static#l(e){return null==e||"object"!=typeof e}static#s(e){return 1==e[t$1]}};i$1=r$1;
30
+ let e$1 = class e extends Error{constructor(e){super(e),this.name="TreeNavigateError";}};const t$1="--tree-leaf";var i$1;let r$1 = class r{#e={};#t=".";#i=false;#r=true;constructor(){}static from(e,t={}){const r=new i$1;return r.#e=e,r.#t=t.delimiter??".",r.#i=t.allowWildcard??false,r.#r=t.allowRecursiveWildcard??true,r}subtree(t){const r=t.split(this.#t),a=this.#a(r,this.#e);if(!a)throw new e$1(`Path '${t}' does not exist`);if(i$1.#l(a.value)||i$1.#s(a.value))throw new e$1(`Path '${t}' is not a subtree`);const l=a.path.reduce((e,t)=>e[t],this.#e),s=new i$1;return s.#e=l,s.#t=this.#t,s.#i=this.#i,s}get(e,t){return this.walk(e,t)?.value??null}walk(e,t={}){if(""===e)return t.allowIntermediate?{value:this.#e,path:[]}:null;const r=e.split(this.#t),a=this.#a(r,this.#e);if(!a)return null;const l=a.value;return i$1.#l(l)?a:i$1.#s(l)?{value:l.value,path:a.path}:t.allowIntermediate?a:null}trace(e){const t=e.split(this.#t),r={treePath:[]},a=this.#a(t,this.#e,r);let l,s;if(a){const e=a.value;return i$1.#l(e)?(l=true,s=e):i$1.#s(e)?(l=true,s=e.value):(l=false,s=e),{find:true,isLeaf:l,value:s,nodePath:a?.path??[],tracePath:r.treePath,untracePath:t}}return {find:false,isLeaf:false,value:void 0,nodePath:[],tracePath:r.treePath,untracePath:t}}#a(e,t,r={treePath:[]}){const{treePath:a}=r,l=e.shift();if(null==l)return {value:t,path:[]};if("object"!=typeof t||i$1.#s(t))return e.unshift(l),null;if(a.push(l),l in t){const i=this.#a(e,t[l],r);if(i)return {value:i.value,path:[l,...i.path]}}else if(this.#i){if("*"in t){const i=this.#a(e,t["*"],r);if(i)return {value:i.value,path:["*",...i.path]}}if(this.#r&&"**/*"in t)return {value:t["**/*"],path:["**/*"]}}return null}static#l(e){return null==e||"object"!=typeof e}static#s(e){return 1==e[t$1]}};i$1=r$1;
31
31
 
32
32
  class JSONAccessorError extends Error {
33
33
  constructor(message) {
@@ -35,6 +35,18 @@ class JSONAccessorError extends Error {
35
35
  this.name = 'AccessorError';
36
36
  }
37
37
  }
38
+ class UnserializableTypeError extends JSONAccessorError {
39
+ constructor(key, value) {
40
+ super(`Unserializable data: '${value}' for field '${key}'`);
41
+ this.name = 'UnserializableTypeError';
42
+ }
43
+ }
44
+ class IncompatibleTypeError extends JSONAccessorError {
45
+ constructor(message) {
46
+ super(message);
47
+ this.name = 'IncompatibleTypeError';
48
+ }
49
+ }
38
50
 
39
51
  const JSON_TYPE_FLAG = '--json-type';
40
52
 
@@ -188,10 +200,18 @@ function isObject(value) {
188
200
  return typeof value === 'object' && value !== null && !Array.isArray(value);
189
201
  }
190
202
 
191
- function concatDotPath(prefix, key) {
192
- return (prefix && prefix.length > 0) ? `${prefix}.${key}` : key;
203
+ /**
204
+ * 경로 문자열을 연결하여 반환
205
+ */
206
+ function dotJoin(prefix, key) {
207
+ return (prefix != null && prefix.length > 0) ? `${prefix}.${key}` : key;
193
208
  }
209
+ /**
210
+ * 값이 json-accessor 호환 가능 타입인지 확인하고 타입명 반환, 호환 불가능할 경우 null 반환
211
+ */
194
212
  function getJSONTypeName(value) {
213
+ if (value == null)
214
+ return 'null';
195
215
  if (Array.isArray(value))
196
216
  return 'array';
197
217
  const typeName = typeof value;
@@ -216,10 +236,17 @@ class Flattener {
216
236
  this.typeChecker = typeChecker;
217
237
  }
218
238
  /**
219
- * key-value 대한 경로 유효성 검사
239
+ * key-value 쌍에 대한 경로 유효성 검사후 `Array<[key, value]>` 형태로 반환
240
+ *
241
+ * @param key - dot(.)으로 구분된 경로 문자열
242
+ * @param value - 유효성 검사를 위한 값
243
+ * @return 인자를 [[key, value]] 형태로 반환
220
244
  */
221
245
  transform(key, value) {
246
+ // [[key, value]] 를 그대로 반환하는건 flat()과의 호환을 위함
247
+ // 추후 구현에서 한 key에서 여러 결과를 리턴하는 등의 확장 가능성이 있음
222
248
  if (key === '') {
249
+ // 루트 경로는 비허용
223
250
  throw new JSONAccessorError(`Invalid path: ${key}`);
224
251
  }
225
252
  if (this.navigate == null) {
@@ -230,7 +257,7 @@ class Flattener {
230
257
  if (traceResult.isLeaf) {
231
258
  const node = traceResult.value;
232
259
  this.typeChecker.check(key, value, node);
233
- if (node.type === 'struct') {
260
+ if (value != null && node.type === 'struct') {
234
261
  // struct 형식인 value에 대한 구조 검사
235
262
  this.flatStruct({
236
263
  target: value,
@@ -241,6 +268,8 @@ class Flattener {
241
268
  return [[key, value]];
242
269
  }
243
270
  else {
271
+ // 경로는 존재하지만 노드의 중간인 경우
272
+ // value가 object 형태라면 flat()을 통해 유효성 검사를 진행
244
273
  if (!isObject(value)) {
245
274
  throw new JSONAccessorError(`Field '${key}' is not allowed`);
246
275
  }
@@ -250,34 +279,41 @@ class Flattener {
250
279
  });
251
280
  }
252
281
  }
282
+ // 경로상 존재하지 않는 경우
283
+ else if (traceResult.tracePath.length === 0) {
284
+ throw new JSONAccessorError(`Field '${key}' is not allowed`);
285
+ }
286
+ // 중간 경로까지는 존재하는 경우
253
287
  else {
254
- // 경로상 존재하지 않는 경우
255
- // 닿을 수 있는 마지막 노드의 타입이 struct, any 라면 허용됨
256
288
  const { tracePath } = traceResult;
257
289
  const reached = tracePath.join(DELIMITER);
258
290
  const node = this.navigate.get(reached);
291
+ // 닿을 수 있는 마지막 노드의 타입이 struct, any 라면 허용됨
259
292
  if (!node || (node.type !== 'struct' && node.type !== 'any')) {
260
293
  throw new JSONAccessorError(`Field '${key}' is not allowed`);
261
294
  }
262
295
  return [[key, value]];
263
296
  }
264
297
  }
265
- flat({ target, prefix, }) {
298
+ /**
299
+ * 객체를 평탄화하여 `Array<[key, value]>`로 반환
300
+ *
301
+ * 유효성 검증을 포함함
302
+ */
303
+ flat({ target, prefix }) {
266
304
  if (!this.navigate) {
267
305
  return this.flatWithoutNavigate({ target, prefix });
268
306
  }
269
307
  const navigate = this.navigate;
270
- return Object.entries(target).flatMap(([key, value]) => {
271
- const newKey = concatDotPath(prefix, key);
308
+ return Object.entries(target)
309
+ .flatMap(([key, value]) => {
310
+ const newKey = dotJoin(prefix, key);
272
311
  const node = navigate.get(newKey, { allowIntermediate: true });
273
312
  if (node == null) {
274
313
  throw new JSONAccessorError(`Field '${newKey}' is not allowed`);
275
314
  }
276
315
  else if (isJSONTypeData(node)) {
277
316
  this.typeChecker.check(newKey, value, node);
278
- // @TODO : 배열 타입에 대한 세부 처리 필요
279
- // 현재는 배열 경로까지만 flat이 이루어지므로
280
- // get/set 시 배열 전체를 가져오거나 덮어쓰게됨
281
317
  if (node.type === 'any') {
282
318
  return this.flatWithoutNavigate({
283
319
  target: value,
@@ -291,6 +327,12 @@ class Flattener {
291
327
  structData: node,
292
328
  });
293
329
  }
330
+ else if (node.type === 'array') {
331
+ // @TODO : 배열 타입에 대한 세부 처리 필요
332
+ // 현재는 배열 경로까지만 flat이 이루어지므로
333
+ // get/set 시 배열 전체를 가져오거나 덮어쓰게됨
334
+ return [[newKey, value]];
335
+ }
294
336
  else {
295
337
  return [[newKey, value]];
296
338
  }
@@ -302,7 +344,11 @@ class Flattener {
302
344
  });
303
345
  }
304
346
  else {
305
- throw new JSONAccessorError(`Logic error: ${newKey}`);
347
+ // 가져온 값이 JSONTypeData(탐색한 노드의 ) 도 아니고
348
+ // 일반 object(중간 노드)도 아닌 다른 타입인 경우
349
+ //
350
+ // TreeNavigate<JSONTypeData> 에서 가져온 결과에선 나올 수 없으므로 데이터 오염이라고 판단
351
+ throw new JSONAccessorError(`Data corrupted: unexpected node type encountered. key: '${newKey}', value: ${value} (${typeof value})`);
306
352
  }
307
353
  });
308
354
  }
@@ -311,8 +357,9 @@ class Flattener {
311
357
  */
312
358
  flatStruct({ target, prefix, structData }) {
313
359
  // struct는 내부 구조에 대한 구조 검증이 없으므로 유효성 검증 없이 단순 평탄화
314
- return Object.entries(target).flatMap(([key, value]) => {
315
- const newKey = concatDotPath(prefix, key);
360
+ return Object.entries(target)
361
+ .flatMap(([key, value]) => {
362
+ const newKey = dotJoin(prefix, key);
316
363
  if (isObject(value)) {
317
364
  return this.flatWithoutNavigate({
318
365
  target: value,
@@ -325,11 +372,12 @@ class Flattener {
325
372
  });
326
373
  }
327
374
  /**
328
- * 유효성 검증 없이 flatten 수행
375
+ * 유효성 검증 없이 평탄화 수행
329
376
  */
330
377
  flatWithoutNavigate({ target, prefix }) {
331
- return Object.entries(target).flatMap(([key, value]) => {
332
- const newKey = concatDotPath(prefix, key);
378
+ return Object.entries(target)
379
+ .flatMap(([key, value]) => {
380
+ const newKey = dotJoin(prefix, key);
333
381
  if (isObject(value)) {
334
382
  return this.flatWithoutNavigate({
335
383
  target: value,
@@ -350,20 +398,26 @@ class CompatibilityChecker {
350
398
  if (!this.isCompatible(value, typeData)) {
351
399
  const typeName = getJSONTypeName(value);
352
400
  if (typeName == null) {
353
- throw new JSONAccessorError(`Invalid data type: ${typeName} in '${key}'`);
401
+ throw new UnserializableTypeError(key, value);
354
402
  }
355
403
  else if (typeName === 'null') {
356
- throw new JSONAccessorError(`Field '${key}' is not nullable`);
404
+ throw new IncompatibleTypeError(`Incompatible type for field '${key}': expected '${typeData.type}' and not nullable, received null`);
357
405
  }
358
406
  else if (typeName === 'array' && typeData.type === 'array') {
359
- throw new JSONAccessorError(`Field '${key}' array structure is incompatible`);
407
+ throw new IncompatibleTypeError(`Incompatible array structure for field '${key}'`);
408
+ }
409
+ else if (typeData.type === 'union') {
410
+ const expected = this.getUnionTypeNames(typeData);
411
+ throw new IncompatibleTypeError(`Incompatible type for field '${key}': expected one of (${expected.join(' | ')}), received ${value} ('${typeName}')`);
360
412
  }
361
413
  else {
362
- throw new JSONAccessorError(`Field '${key}' must be a '${typeData.type}' but '${typeName}'`);
414
+ throw new IncompatibleTypeError(`Incompatible type for field '${key}': expected '${typeData.type}', received '${typeName}'`);
363
415
  }
364
416
  }
365
417
  }
366
418
  isCompatible(target, jsonTypeData) {
419
+ // jsonTypeData 타입이 primitive 인 경우
420
+ // union 타입 검사 시 isCompatible() 가 다시 호출된 경우 발생
367
421
  if (typeof jsonTypeData !== 'object') {
368
422
  return target === jsonTypeData;
369
423
  }
@@ -372,12 +426,12 @@ class CompatibilityChecker {
372
426
  if (targetType === null) {
373
427
  return false;
374
428
  }
375
- else if (targetType === 'null') {
376
- return jsonTypeData.nullable;
377
- }
378
429
  else if (jsonTypeData.type === 'any') {
379
430
  return true;
380
431
  }
432
+ else if (targetType === 'null') {
433
+ return jsonTypeData.nullable;
434
+ }
381
435
  else if (jsonTypeData.type === 'union') {
382
436
  for (const candidate of jsonTypeData.candidates) {
383
437
  if (this.isCompatible(target, candidate)) {
@@ -402,9 +456,7 @@ class CompatibilityChecker {
402
456
  }
403
457
  }
404
458
  isArrayCompatible(array, arrayTypeData) {
405
- if (!arrayTypeData.strict)
406
- return true;
407
- if (!arrayTypeData.element)
459
+ if (!arrayTypeData.strict || !arrayTypeData.element)
408
460
  return true;
409
461
  for (const ele of array) {
410
462
  if (!this.isCompatible(ele, arrayTypeData.element)) {
@@ -428,6 +480,16 @@ class CompatibilityChecker {
428
480
  return false;
429
481
  }
430
482
  }
483
+ getUnionTypeNames(union) {
484
+ return union.candidates.map((c) => {
485
+ if (typeof c === 'object') {
486
+ return c.type;
487
+ }
488
+ else {
489
+ return c.toString();
490
+ }
491
+ });
492
+ }
431
493
  }
432
494
 
433
495
  class SchemaFlattener {
@@ -498,6 +560,56 @@ class MockJSONFS {
498
560
  }
499
561
  }
500
562
 
563
+ class DefaultValueProvider {
564
+ #navigate;
565
+ constructor(navigate = null) {
566
+ this.#navigate = navigate;
567
+ }
568
+ get(key) {
569
+ if (this.#navigate == null) {
570
+ return undefined;
571
+ }
572
+ const result = this.#navigate.walk(key, { allowIntermediate: true });
573
+ if (result == null) {
574
+ return undefined;
575
+ }
576
+ else if (isJSONTypeData(result.value)) {
577
+ const defaultValue = result.value.default_value;
578
+ return ((defaultValue != null)
579
+ ? defaultValue
580
+ : undefined);
581
+ }
582
+ else {
583
+ // for (const [key, value] of Object.entries(result.value)) {
584
+ // console.log(key, value);
585
+ // }
586
+ return this.#getDefaultData(result.value);
587
+ }
588
+ }
589
+ #getDefaultData(typeData) {
590
+ if (t$1 in typeData) {
591
+ if (typeData.value.default_value != null) {
592
+ return typeData.value.default_value;
593
+ }
594
+ return undefined;
595
+ }
596
+ else {
597
+ let hasKey = false;
598
+ const result = {};
599
+ for (const [key, value] of Object.entries(typeData)) {
600
+ const defaultValue = this.#getDefaultData(value);
601
+ if (defaultValue !== undefined) {
602
+ result[key] = defaultValue;
603
+ hasKey = true;
604
+ }
605
+ }
606
+ return (hasKey
607
+ ? result
608
+ : undefined);
609
+ }
610
+ }
611
+ }
612
+
501
613
  class JSONAccessor {
502
614
  static anyJSONType = JSONType.Any().nullable().value;
503
615
  filePath;
@@ -506,6 +618,7 @@ class JSONAccessor {
506
618
  jsonFS = new JSONFS();
507
619
  #tree = null;
508
620
  #flatter;
621
+ #defaultValueProvider;
509
622
  #isDropped = false;
510
623
  #changed = true;
511
624
  constructor(filePath, tree = null) {
@@ -515,9 +628,11 @@ class JSONAccessor {
515
628
  this.#tree = tree;
516
629
  this.explorer = r$1.from(tree, { delimiter: '.', allowWildcard: true, allowRecursiveWildcard: false });
517
630
  this.#flatter = new SchemaFlattener(this.explorer);
631
+ this.#defaultValueProvider = new DefaultValueProvider(this.explorer);
518
632
  }
519
633
  else {
520
634
  this.#flatter = new SchemaFlattener();
635
+ this.#defaultValueProvider = new DefaultValueProvider();
521
636
  }
522
637
  }
523
638
  get tree() {
@@ -569,13 +684,20 @@ class JSONAccessor {
569
684
  }
570
685
  getOne(key) {
571
686
  this.#ensureNotDropped();
572
- return this.#getData(key);
687
+ let value = this.#getData(key);
688
+ if (value == null) {
689
+ value = this.#defaultValueProvider.get(key);
690
+ }
691
+ return value;
573
692
  }
574
693
  get(...keys) {
575
694
  this.#ensureNotDropped();
576
695
  const result = {};
577
696
  for (const key of keys) {
578
- const value = this.#getData(key);
697
+ let value = this.#getData(key);
698
+ if (value == null) {
699
+ value = this.#defaultValueProvider.get(key);
700
+ }
579
701
  const resolved = resolveNestedRef(result, key, true);
580
702
  resolved.parent[resolved.key] = value;
581
703
  }