apimo.js 1.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.
- package/.github/workflows/ci.yml +37 -0
- package/.github/workflows/publish.yml +69 -0
- package/.idea/apimo.js.iml +13 -0
- package/.idea/copilotDiffState.xml +43 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/jsLinters/eslint.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/prettier.xml +6 -0
- package/.idea/vcs.xml +6 -0
- package/README.md +91 -0
- package/dist/src/consts/catalogs.d.ts +2 -0
- package/dist/src/consts/catalogs.js +53 -0
- package/dist/src/consts/languages.d.ts +2 -0
- package/dist/src/consts/languages.js +20 -0
- package/dist/src/core/api.d.ts +389 -0
- package/dist/src/core/api.js +157 -0
- package/dist/src/core/api.test.d.ts +1 -0
- package/dist/src/core/api.test.js +246 -0
- package/dist/src/core/converters.d.ts +4 -0
- package/dist/src/core/converters.js +4 -0
- package/dist/src/schemas/agency.d.ts +416 -0
- package/dist/src/schemas/agency.js +61 -0
- package/dist/src/schemas/common.d.ts +153 -0
- package/dist/src/schemas/common.js +47 -0
- package/dist/src/schemas/internal.d.ts +3 -0
- package/dist/src/schemas/internal.js +11 -0
- package/dist/src/schemas/property.d.ts +1500 -0
- package/dist/src/schemas/property.js +238 -0
- package/dist/src/services/storage/dummy.cache.d.ts +10 -0
- package/dist/src/services/storage/dummy.cache.js +28 -0
- package/dist/src/services/storage/dummy.cache.test.d.ts +1 -0
- package/dist/src/services/storage/dummy.cache.test.js +96 -0
- package/dist/src/services/storage/filesystem.cache.d.ts +18 -0
- package/dist/src/services/storage/filesystem.cache.js +85 -0
- package/dist/src/services/storage/filesystem.cache.test.d.ts +1 -0
- package/dist/src/services/storage/filesystem.cache.test.js +197 -0
- package/dist/src/services/storage/memory.cache.d.ts +20 -0
- package/dist/src/services/storage/memory.cache.js +62 -0
- package/dist/src/services/storage/memory.cache.test.d.ts +1 -0
- package/dist/src/services/storage/memory.cache.test.js +80 -0
- package/dist/src/services/storage/types.d.ts +16 -0
- package/dist/src/services/storage/types.js +4 -0
- package/dist/src/types/index.d.ts +4 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/utils/url.d.ts +14 -0
- package/dist/src/utils/url.js +11 -0
- package/dist/src/utils/url.test.d.ts +1 -0
- package/dist/src/utils/url.test.js +18 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +6 -0
- package/eslint.config.mjs +3 -0
- package/package.json +45 -0
- package/src/consts/catalogs.ts +55 -0
- package/src/consts/languages.ts +22 -0
- package/src/core/api.test.ts +308 -0
- package/src/core/api.ts +230 -0
- package/src/core/converters.ts +7 -0
- package/src/schemas/agency.ts +66 -0
- package/src/schemas/common.ts +67 -0
- package/src/schemas/internal.ts +13 -0
- package/src/schemas/property.ts +257 -0
- package/src/services/storage/dummy.cache.test.ts +110 -0
- package/src/services/storage/dummy.cache.ts +21 -0
- package/src/services/storage/filesystem.cache.test.ts +243 -0
- package/src/services/storage/filesystem.cache.ts +94 -0
- package/src/services/storage/memory.cache.test.ts +94 -0
- package/src/services/storage/memory.cache.ts +69 -0
- package/src/services/storage/types.ts +20 -0
- package/src/types/index.ts +5 -0
- package/src/utils/url.test.ts +21 -0
- package/src/utils/url.ts +27 -0
- package/tsconfig.json +13 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
11
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
12
|
+
import * as os from 'node:os';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
15
|
+
import { FilesystemCache } from './filesystem.cache';
|
|
16
|
+
import { CacheExpiredError } from './types';
|
|
17
|
+
describe('cache - Filesystem', () => {
|
|
18
|
+
let tempDir;
|
|
19
|
+
let cache;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
// Create a unique temporary directory for each test
|
|
22
|
+
tempDir = path.join(os.tmpdir(), `filesystem-cache-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
23
|
+
mkdirSync(tempDir, { recursive: true });
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
// Clean up the temporary directory after each test
|
|
27
|
+
if (existsSync(tempDir)) {
|
|
28
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
describe('constructor', () => {
|
|
32
|
+
it('should use default cache location and expiration when no settings provided', () => {
|
|
33
|
+
// eslint-disable-next-line no-new
|
|
34
|
+
new FilesystemCache();
|
|
35
|
+
expect(existsSync('./cache/catalogs')).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
it('should create custom cache directory when provided', () => {
|
|
38
|
+
const customPath = path.join(tempDir, 'custom-cache');
|
|
39
|
+
cache = new FilesystemCache({ path: customPath });
|
|
40
|
+
expect(existsSync(customPath)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
it('should use custom expiration time when provided', () => {
|
|
43
|
+
const customExpiration = 60000;
|
|
44
|
+
cache = new FilesystemCache({
|
|
45
|
+
path: tempDir,
|
|
46
|
+
cacheExpirationMs: customExpiration,
|
|
47
|
+
});
|
|
48
|
+
// The expiration time is private, but we can test its effect
|
|
49
|
+
expect(cache).toBeInstanceOf(FilesystemCache);
|
|
50
|
+
});
|
|
51
|
+
it('should create nested directories recursively', () => {
|
|
52
|
+
const nestedPath = path.join(tempDir, 'level1', 'level2', 'cache');
|
|
53
|
+
cache = new FilesystemCache({ path: nestedPath });
|
|
54
|
+
expect(existsSync(nestedPath)).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('setEntries and getEntry', () => {
|
|
58
|
+
const culture = 'en';
|
|
59
|
+
const entries = [
|
|
60
|
+
{ id: 1, name: 'Item 1', name_plurial: 'Items 1' },
|
|
61
|
+
{ id: 2, name: 'Item 2', name_plurial: 'Items 2' },
|
|
62
|
+
];
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
cache = new FilesystemCache({ path: tempDir });
|
|
65
|
+
});
|
|
66
|
+
it('should store and retrieve entries correctly', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
67
|
+
const catalogName = 'book_step';
|
|
68
|
+
yield cache.setEntries(catalogName, culture, entries);
|
|
69
|
+
const entry1 = yield cache.getEntry(catalogName, culture, 1);
|
|
70
|
+
const entry2 = yield cache.getEntry(catalogName, culture, 2);
|
|
71
|
+
expect(entry1).toEqual({ name: 'Item 1', namePlural: 'Items 1' });
|
|
72
|
+
expect(entry2).toEqual({ name: 'Item 2', namePlural: 'Items 2' });
|
|
73
|
+
}));
|
|
74
|
+
it('should create cache file with correct format', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
75
|
+
const catalogName = 'book_step';
|
|
76
|
+
yield cache.setEntries(catalogName, culture, entries);
|
|
77
|
+
const filePath = path.join(tempDir, `${catalogName}-${culture}.json`);
|
|
78
|
+
expect(existsSync(filePath)).toBe(true);
|
|
79
|
+
const fileContent = yield readFile(filePath, 'utf-8');
|
|
80
|
+
const parsed = JSON.parse(fileContent);
|
|
81
|
+
expect(parsed).toHaveProperty('timestamp');
|
|
82
|
+
expect(parsed).toHaveProperty('cache');
|
|
83
|
+
expect(parsed.cache).toEqual({
|
|
84
|
+
1: { name: 'Item 1', namePlural: 'Items 1' },
|
|
85
|
+
2: { name: 'Item 2', namePlural: 'Items 2' },
|
|
86
|
+
});
|
|
87
|
+
expect(typeof parsed.timestamp).toBe('number');
|
|
88
|
+
}));
|
|
89
|
+
it('should return null for non-existent entry', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
90
|
+
const catalogName = 'book_step';
|
|
91
|
+
yield cache.setEntries(catalogName, culture, entries);
|
|
92
|
+
const entry = yield cache.getEntry(catalogName, culture, 999);
|
|
93
|
+
expect(entry).toBeNull();
|
|
94
|
+
}));
|
|
95
|
+
it('should throw CacheExpiredError when cache file does not exist', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
96
|
+
const catalogName = 'book_step';
|
|
97
|
+
yield expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError);
|
|
98
|
+
}));
|
|
99
|
+
it('should throw CacheExpiredError when cache has expired', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
100
|
+
const catalogName = 'book_step';
|
|
101
|
+
const expiredCache = new FilesystemCache({
|
|
102
|
+
path: tempDir,
|
|
103
|
+
cacheExpirationMs: 1,
|
|
104
|
+
});
|
|
105
|
+
yield expiredCache.setEntries(catalogName, culture, entries);
|
|
106
|
+
yield new Promise(resolve => setTimeout(resolve, 10));
|
|
107
|
+
yield expect(expiredCache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError);
|
|
108
|
+
}));
|
|
109
|
+
it('should handle different catalog and culture combinations', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
110
|
+
yield cache.setEntries('book_step', 'en', entries);
|
|
111
|
+
yield cache.setEntries('property_land', 'fr', entries);
|
|
112
|
+
const entry1 = yield cache.getEntry('book_step', 'en', 1);
|
|
113
|
+
const entry2 = yield cache.getEntry('property_land', 'fr', 1);
|
|
114
|
+
expect(entry1).toEqual({ name: 'Item 1', namePlural: 'Items 1' });
|
|
115
|
+
expect(entry2).toEqual({ name: 'Item 1', namePlural: 'Items 1' });
|
|
116
|
+
yield expect(cache.getEntry('book_step', 'fr', 1)).rejects.toThrow(CacheExpiredError);
|
|
117
|
+
// Verify separate files were created
|
|
118
|
+
expect(existsSync(path.join(tempDir, 'book_step-en.json'))).toBe(true);
|
|
119
|
+
expect(existsSync(path.join(tempDir, 'property_land-fr.json'))).toBe(true);
|
|
120
|
+
expect(existsSync(path.join(tempDir, 'book_step-fr.json'))).toBe(false);
|
|
121
|
+
}));
|
|
122
|
+
it('should overwrite existing cache when setting new entries', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
123
|
+
const catalogName = 'property_land';
|
|
124
|
+
yield cache.setEntries(catalogName, culture, entries);
|
|
125
|
+
const newEntries = [{ id: 3, name: 'New Item', name_plurial: 'New Items' }];
|
|
126
|
+
yield cache.setEntries(catalogName, culture, newEntries);
|
|
127
|
+
const newEntry = yield cache.getEntry(catalogName, culture, 3);
|
|
128
|
+
expect(newEntry).toEqual({ name: 'New Item', namePlural: 'New Items' });
|
|
129
|
+
const oldEntry = yield cache.getEntry(catalogName, culture, 1);
|
|
130
|
+
expect(oldEntry).toBeNull();
|
|
131
|
+
}));
|
|
132
|
+
it('should handle entries with undefined name_plurial', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
133
|
+
const catalogName = 'book_step';
|
|
134
|
+
const entriesWithUndefined = [
|
|
135
|
+
{ id: 1, name: 'Item 1', name_plurial: undefined },
|
|
136
|
+
{ id: 2, name: 'Item 2', name_plurial: 'Items 2' },
|
|
137
|
+
];
|
|
138
|
+
yield cache.setEntries(catalogName, culture, entriesWithUndefined);
|
|
139
|
+
const entry1 = yield cache.getEntry(catalogName, culture, 1);
|
|
140
|
+
const entry2 = yield cache.getEntry(catalogName, culture, 2);
|
|
141
|
+
expect(entry1).toEqual({ name: 'Item 1', namePlural: undefined });
|
|
142
|
+
expect(entry2).toEqual({ name: 'Item 2', namePlural: 'Items 2' });
|
|
143
|
+
}));
|
|
144
|
+
it('should handle large entry IDs correctly', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
145
|
+
const catalogName = 'book_step';
|
|
146
|
+
const largeIdEntries = [
|
|
147
|
+
{ id: 999999999, name: 'Large ID Item', name_plurial: 'Large ID Items' },
|
|
148
|
+
];
|
|
149
|
+
yield cache.setEntries(catalogName, culture, largeIdEntries);
|
|
150
|
+
const entry = yield cache.getEntry(catalogName, culture, 999999999);
|
|
151
|
+
expect(entry).toEqual({ name: 'Large ID Item', namePlural: 'Large ID Items' });
|
|
152
|
+
}));
|
|
153
|
+
it('should persist cache across multiple FilesystemCache instances', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
154
|
+
const catalogName = 'book_step';
|
|
155
|
+
// Create cache with first instance
|
|
156
|
+
const cache1 = new FilesystemCache({ path: tempDir });
|
|
157
|
+
yield cache1.setEntries(catalogName, culture, entries);
|
|
158
|
+
// Access cache with second instance
|
|
159
|
+
const cache2 = new FilesystemCache({ path: tempDir });
|
|
160
|
+
const entry = yield cache2.getEntry(catalogName, culture, 1);
|
|
161
|
+
expect(entry).toEqual({ name: 'Item 1', namePlural: 'Items 1' });
|
|
162
|
+
}));
|
|
163
|
+
});
|
|
164
|
+
describe('error handling', () => {
|
|
165
|
+
beforeEach(() => {
|
|
166
|
+
cache = new FilesystemCache({ path: tempDir });
|
|
167
|
+
});
|
|
168
|
+
it('should handle malformed JSON files gracefully', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
169
|
+
const catalogName = 'book_step';
|
|
170
|
+
const culture = 'en';
|
|
171
|
+
const filePath = path.join(tempDir, `${catalogName}-${culture}.json`);
|
|
172
|
+
// Write malformed JSON
|
|
173
|
+
yield writeFile(filePath, 'invalid json content');
|
|
174
|
+
yield expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow();
|
|
175
|
+
}));
|
|
176
|
+
it('should handle files with missing timestamp', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
177
|
+
const catalogName = 'book_step';
|
|
178
|
+
const culture = 'en';
|
|
179
|
+
const filePath = path.join(tempDir, `${catalogName}-${culture}.json`);
|
|
180
|
+
// Write JSON without timestamp
|
|
181
|
+
yield writeFile(filePath, JSON.stringify({
|
|
182
|
+
cache: { 1: { name: 'Test', namePlural: 'Tests' } },
|
|
183
|
+
}));
|
|
184
|
+
yield expect(cache.getEntry(catalogName, culture, 1)).resolves.toStrictEqual({ name: 'Test', namePlural: 'Tests' });
|
|
185
|
+
}));
|
|
186
|
+
it('should handle files with missing cache property', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
187
|
+
const catalogName = 'book_step';
|
|
188
|
+
const culture = 'en';
|
|
189
|
+
const filePath = path.join(tempDir, `${catalogName}-${culture}.json`);
|
|
190
|
+
// Write JSON without cache property
|
|
191
|
+
yield writeFile(filePath, JSON.stringify({
|
|
192
|
+
timestamp: Date.now(),
|
|
193
|
+
}));
|
|
194
|
+
yield expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow();
|
|
195
|
+
}));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CatalogName } from '../../consts/catalogs';
|
|
2
|
+
import type { ApiCulture } from '../../consts/languages';
|
|
3
|
+
import type { CatalogEntry } from '../../schemas/common';
|
|
4
|
+
import type { ApiCacheAdapter, CatalogEntryName } from './types';
|
|
5
|
+
type Memory = Map<string, {
|
|
6
|
+
timestamp: number;
|
|
7
|
+
cache: Map<number, CatalogEntryName>;
|
|
8
|
+
}>;
|
|
9
|
+
export declare class MemoryCache implements ApiCacheAdapter {
|
|
10
|
+
readonly cacheExpirationMs: number;
|
|
11
|
+
readonly _MEMORY: Memory;
|
|
12
|
+
constructor(settings?: {
|
|
13
|
+
cacheExpirationMs?: number;
|
|
14
|
+
});
|
|
15
|
+
setEntries(catalogName: CatalogName, culture: ApiCulture, entries: CatalogEntry[]): Promise<void>;
|
|
16
|
+
getEntry(catalogName: CatalogName, culture: ApiCulture, id: number): Promise<CatalogEntryName | null>;
|
|
17
|
+
getEntries(catalogName: CatalogName, culture: ApiCulture): Promise<CatalogEntry[]>;
|
|
18
|
+
private getCacheKey;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { CacheExpiredError } from './types';
|
|
11
|
+
const MS_IN_ONE_WEEK = 7 * 24 * 60 * 60 * 1000;
|
|
12
|
+
export class MemoryCache {
|
|
13
|
+
constructor(settings) {
|
|
14
|
+
var _a;
|
|
15
|
+
this.cacheExpirationMs = (_a = settings === null || settings === void 0 ? void 0 : settings.cacheExpirationMs) !== null && _a !== void 0 ? _a : MS_IN_ONE_WEEK;
|
|
16
|
+
this._MEMORY = new Map();
|
|
17
|
+
}
|
|
18
|
+
setEntries(catalogName, culture, entries) {
|
|
19
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20
|
+
const memoryEntry = new Map(entries.map(({ id, name, name_plurial }) => [id, {
|
|
21
|
+
name,
|
|
22
|
+
namePlural: name_plurial,
|
|
23
|
+
}]));
|
|
24
|
+
this._MEMORY.set(this.getCacheKey(catalogName, culture), {
|
|
25
|
+
timestamp: Date.now(),
|
|
26
|
+
cache: memoryEntry,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
getEntry(catalogName, culture, id) {
|
|
31
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
32
|
+
var _a;
|
|
33
|
+
const memoryEntry = this._MEMORY.get(this.getCacheKey(catalogName, culture));
|
|
34
|
+
if (!memoryEntry) {
|
|
35
|
+
throw new CacheExpiredError();
|
|
36
|
+
}
|
|
37
|
+
if (memoryEntry.timestamp + this.cacheExpirationMs < Date.now()) {
|
|
38
|
+
throw new CacheExpiredError();
|
|
39
|
+
}
|
|
40
|
+
return (_a = memoryEntry.cache.get(id)) !== null && _a !== void 0 ? _a : null;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
getEntries(catalogName, culture) {
|
|
44
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
45
|
+
const memoryEntry = this._MEMORY.get(this.getCacheKey(catalogName, culture));
|
|
46
|
+
if (!memoryEntry) {
|
|
47
|
+
throw new CacheExpiredError();
|
|
48
|
+
}
|
|
49
|
+
if (memoryEntry.timestamp + this.cacheExpirationMs < Date.now()) {
|
|
50
|
+
throw new CacheExpiredError();
|
|
51
|
+
}
|
|
52
|
+
return Array.from(memoryEntry.cache.entries()).map(([id, { name, namePlural }]) => ({
|
|
53
|
+
id,
|
|
54
|
+
name,
|
|
55
|
+
name_plurial: namePlural,
|
|
56
|
+
}));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
getCacheKey(catalogName, culture) {
|
|
60
|
+
return `${catalogName}.${culture}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import { MemoryCache } from './memory.cache';
|
|
12
|
+
describe('cache - Memory', () => {
|
|
13
|
+
describe('constructor', () => {
|
|
14
|
+
it('should use default expiration time when no settings provided', () => {
|
|
15
|
+
const defaultCache = new MemoryCache();
|
|
16
|
+
expect(defaultCache.cacheExpirationMs).toBe(7 * 24 * 60 * 60 * 1000);
|
|
17
|
+
});
|
|
18
|
+
it('should use custom expiration time when provided', () => {
|
|
19
|
+
const customExpiration = 60000;
|
|
20
|
+
const customCache = new MemoryCache({ cacheExpirationMs: customExpiration });
|
|
21
|
+
expect(customCache.cacheExpirationMs).toBe(customExpiration);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('setEntries and getEntry', () => {
|
|
25
|
+
const culture = 'en';
|
|
26
|
+
const entries = [
|
|
27
|
+
{ id: 1, name: 'Item 1', name_plurial: 'Items 1' },
|
|
28
|
+
{ id: 2, name: 'Item 2', name_plurial: 'Items 2' },
|
|
29
|
+
];
|
|
30
|
+
it('should store and retrieve entries correctly', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
31
|
+
const cache = new MemoryCache();
|
|
32
|
+
const catalogName = 'book_step';
|
|
33
|
+
yield cache.setEntries(catalogName, culture, entries);
|
|
34
|
+
const entry1 = yield cache.getEntry(catalogName, culture, 1);
|
|
35
|
+
const entry2 = yield cache.getEntry(catalogName, culture, 2);
|
|
36
|
+
expect(entry1).toEqual({ name: 'Item 1', namePlural: 'Items 1' });
|
|
37
|
+
expect(entry2).toEqual({ name: 'Item 2', namePlural: 'Items 2' });
|
|
38
|
+
}));
|
|
39
|
+
it('should return null for non-existent entry', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
40
|
+
const cache = new MemoryCache();
|
|
41
|
+
const catalogName = 'book_step';
|
|
42
|
+
yield cache.setEntries(catalogName, culture, entries);
|
|
43
|
+
const entry = yield cache.getEntry(catalogName, culture, 999);
|
|
44
|
+
expect(entry).toBeNull();
|
|
45
|
+
}));
|
|
46
|
+
it('should throw CacheExpiredError when cache entry does not exist', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
47
|
+
const cache = new MemoryCache();
|
|
48
|
+
const catalogName = 'book_step';
|
|
49
|
+
yield expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrowError();
|
|
50
|
+
}));
|
|
51
|
+
it('should throw CacheExpiredError when cache has expired', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
52
|
+
const catalogName = 'book_step';
|
|
53
|
+
const expiredCache = new MemoryCache({ cacheExpirationMs: 1 });
|
|
54
|
+
yield expiredCache.setEntries(catalogName, culture, entries);
|
|
55
|
+
yield new Promise(resolve => setTimeout(resolve, 10));
|
|
56
|
+
yield expect(expiredCache.getEntry(catalogName, culture, 1)).rejects.toThrowError();
|
|
57
|
+
}));
|
|
58
|
+
it('should handle different catalog and culture combinations', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
59
|
+
const cache = new MemoryCache();
|
|
60
|
+
yield cache.setEntries('book_step', 'en', entries);
|
|
61
|
+
yield cache.setEntries('property_land', 'fr', entries);
|
|
62
|
+
const entry1 = yield cache.getEntry('book_step', 'en', 1);
|
|
63
|
+
const entry2 = yield cache.getEntry('property_land', 'fr', 1);
|
|
64
|
+
expect(entry1).toEqual({ name: 'Item 1', namePlural: 'Items 1' });
|
|
65
|
+
expect(entry2).toEqual({ name: 'Item 1', namePlural: 'Items 1' });
|
|
66
|
+
yield expect(cache.getEntry('book_step', 'fr', 1)).rejects.toThrowError();
|
|
67
|
+
}));
|
|
68
|
+
it('should overwrite existing cache when setting new entries', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
69
|
+
const cache = new MemoryCache();
|
|
70
|
+
const catalogName = 'property_land';
|
|
71
|
+
yield cache.setEntries(catalogName, culture, entries);
|
|
72
|
+
const newEntries = [{ id: 3, name: 'New Item', name_plurial: 'New Items' }];
|
|
73
|
+
yield cache.setEntries(catalogName, culture, newEntries);
|
|
74
|
+
const newEntry = yield cache.getEntry(catalogName, culture, 3);
|
|
75
|
+
expect(newEntry).toEqual({ name: 'New Item', namePlural: 'New Items' });
|
|
76
|
+
const oldEntry = yield cache.getEntry(catalogName, culture, 1);
|
|
77
|
+
expect(oldEntry).toBeNull();
|
|
78
|
+
}));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CatalogName } from '../../consts/catalogs';
|
|
2
|
+
import type { ApiCulture } from '../../consts/languages';
|
|
3
|
+
import type { CatalogEntry } from '../../schemas/common';
|
|
4
|
+
export interface CatalogEntryName {
|
|
5
|
+
name: string;
|
|
6
|
+
namePlural: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
export interface ApiCacheAdapter {
|
|
9
|
+
setEntries: (catalogName: CatalogName, culture: ApiCulture, entries: CatalogEntry[]) => Promise<void>;
|
|
10
|
+
getEntry: (catalogName: CatalogName, culture: ApiCulture, id: number) => Promise<CatalogEntryName | null>;
|
|
11
|
+
getEntries: (catalogName: CatalogName, culture: ApiCulture) => Promise<CatalogEntry[]>;
|
|
12
|
+
}
|
|
13
|
+
export declare class CacheExpiredError extends Error {
|
|
14
|
+
}
|
|
15
|
+
export declare class NotInCacheError extends Error {
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ApiCulture } from '../consts/languages';
|
|
2
|
+
import type { AdditionalConfig } from '../core/api';
|
|
3
|
+
import type { LACK_OF_DOCUMENTATION } from '../types';
|
|
4
|
+
export interface ApiSearchParams {
|
|
5
|
+
culture?: ApiCulture;
|
|
6
|
+
limit?: number;
|
|
7
|
+
offset?: number;
|
|
8
|
+
timestamp?: number;
|
|
9
|
+
step?: LACK_OF_DOCUMENTATION;
|
|
10
|
+
status?: LACK_OF_DOCUMENTATION;
|
|
11
|
+
group?: LACK_OF_DOCUMENTATION;
|
|
12
|
+
[key: string]: string | number | undefined;
|
|
13
|
+
}
|
|
14
|
+
export declare function makeApiUrl(pathParts: string[], config: AdditionalConfig, searchParams?: Partial<ApiSearchParams>): URL;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function makeApiUrl(pathParts, config, searchParams) {
|
|
2
|
+
const url = new URL(pathParts.join('/'), config.baseUrl);
|
|
3
|
+
if (searchParams) {
|
|
4
|
+
Object.entries(searchParams).forEach(([key, value]) => {
|
|
5
|
+
if (value !== undefined) {
|
|
6
|
+
url.searchParams.append(key, value.toString());
|
|
7
|
+
}
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
return url;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DEFAULT_ADDITIONAL_CONFIG } from '../core/api';
|
|
3
|
+
import { makeApiUrl } from './url';
|
|
4
|
+
const CONFIG = DEFAULT_ADDITIONAL_CONFIG;
|
|
5
|
+
describe('url util', () => {
|
|
6
|
+
it('should, given an array of parts return a complete url', () => {
|
|
7
|
+
const url = makeApiUrl(['hello', 'world'], CONFIG);
|
|
8
|
+
expect(url.href).toBe('https://api.apimo.pro/hello/world');
|
|
9
|
+
});
|
|
10
|
+
it('should, given an array of parts and search params return a complete url', () => {
|
|
11
|
+
const url = makeApiUrl(['hello', 'world'], CONFIG, { culture: 'fr', limit: 10 });
|
|
12
|
+
expect(url.href).toBe('https://api.apimo.pro/hello/world?culture=fr&limit=10');
|
|
13
|
+
});
|
|
14
|
+
it('should, given an array of parts and search params with undefined values, ignore them', () => {
|
|
15
|
+
const url = makeApiUrl(['hello', 'world'], CONFIG, { culture: 'fr', limit: undefined });
|
|
16
|
+
expect(url.href).toBe('https://api.apimo.pro/hello/world?culture=fr');
|
|
17
|
+
});
|
|
18
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "apimo.js",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A wrapper for the Apimo API with catalog caching for building custom Real Estate website using their technologies.",
|
|
5
|
+
"author": "Vitaly Lysen <vitaly@lysen.dev> (https://lysen.dev)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/Neikow/apimo.js",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/Neikow/apimo.js.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Neikow/apimo.js/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"api",
|
|
17
|
+
"apimo",
|
|
18
|
+
"real-estate"
|
|
19
|
+
],
|
|
20
|
+
"main": "/dist/index.js",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "",
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"run": "node dist/index.js",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test-coverage": "vitest run --coverage",
|
|
27
|
+
"lint": "eslint ."
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"bottleneck": "^2.19.5",
|
|
31
|
+
"merge-anything": "^6.0.6",
|
|
32
|
+
"zod": "^3.21.4"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@anatine/zod-mock": "^3.14.0",
|
|
36
|
+
"@antfu/eslint-config": "^5.0.0",
|
|
37
|
+
"@faker-js/faker": "^9.9.0",
|
|
38
|
+
"@types/node": "^24.1.0",
|
|
39
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
40
|
+
"dotenv": "^17.2.1",
|
|
41
|
+
"eslint": "^9.32.0",
|
|
42
|
+
"typescript": "^5.9.2",
|
|
43
|
+
"vitest": "^3.2.4"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const API_CATALOGS = [
|
|
2
|
+
'action_type',
|
|
3
|
+
'action_method',
|
|
4
|
+
'book_step',
|
|
5
|
+
'book_type',
|
|
6
|
+
'construction_step',
|
|
7
|
+
'contact_title',
|
|
8
|
+
'document_type',
|
|
9
|
+
'fees',
|
|
10
|
+
'lead_type',
|
|
11
|
+
'lead_step',
|
|
12
|
+
'property_activity',
|
|
13
|
+
'property_areas',
|
|
14
|
+
'property_adjacency',
|
|
15
|
+
'property_agreement',
|
|
16
|
+
'property_availability',
|
|
17
|
+
'property_building',
|
|
18
|
+
'property_category',
|
|
19
|
+
'property_subcategory',
|
|
20
|
+
'property_condition',
|
|
21
|
+
'property_construction_method',
|
|
22
|
+
'property_floor',
|
|
23
|
+
'property_flooring',
|
|
24
|
+
'property_heating_device',
|
|
25
|
+
'property_heating_access',
|
|
26
|
+
'property_heating_type',
|
|
27
|
+
'property_hot_water_device',
|
|
28
|
+
'property_hot_water_access',
|
|
29
|
+
'property_land',
|
|
30
|
+
'property_lease',
|
|
31
|
+
'property_location',
|
|
32
|
+
'property_orientation',
|
|
33
|
+
'property_period',
|
|
34
|
+
'property_proximity',
|
|
35
|
+
'property_reglementation',
|
|
36
|
+
'property_regulation',
|
|
37
|
+
'property_financial',
|
|
38
|
+
'property_service',
|
|
39
|
+
'property_service_category',
|
|
40
|
+
'property_standing',
|
|
41
|
+
'property_step',
|
|
42
|
+
'property_status',
|
|
43
|
+
'property_type',
|
|
44
|
+
'property_subtype',
|
|
45
|
+
'property_view_landscape',
|
|
46
|
+
'property_view_type',
|
|
47
|
+
'property_waste_water',
|
|
48
|
+
'referral',
|
|
49
|
+
'tags',
|
|
50
|
+
'unit_area',
|
|
51
|
+
'unit_length',
|
|
52
|
+
'user_group',
|
|
53
|
+
] as const
|
|
54
|
+
|
|
55
|
+
export type CatalogName = typeof API_CATALOGS[number]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const API_LANGUAGES = [
|
|
2
|
+
'fr',
|
|
3
|
+
'it',
|
|
4
|
+
'de',
|
|
5
|
+
'es',
|
|
6
|
+
'en',
|
|
7
|
+
'nl',
|
|
8
|
+
'zh',
|
|
9
|
+
'ru',
|
|
10
|
+
'sv',
|
|
11
|
+
'ar',
|
|
12
|
+
'he',
|
|
13
|
+
'nb',
|
|
14
|
+
'pt',
|
|
15
|
+
'fa',
|
|
16
|
+
'lb',
|
|
17
|
+
'km',
|
|
18
|
+
'tr',
|
|
19
|
+
'lo',
|
|
20
|
+
] as const
|
|
21
|
+
|
|
22
|
+
export type ApiCulture = (typeof API_LANGUAGES)[number]
|