apimo.js 1.0.1 → 1.0.2

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 (40) hide show
  1. package/README.md +314 -48
  2. package/dist/src/core/api.d.ts +1 -1
  3. package/dist/src/core/api.js +1 -1
  4. package/dist/src/core/api.test.js +7 -7
  5. package/dist/src/index.d.ts +11 -0
  6. package/dist/src/index.js +10 -0
  7. package/dist/src/schemas/agency.d.ts +8 -8
  8. package/dist/src/schemas/property.d.ts +151 -151
  9. package/package.json +16 -2
  10. package/.github/workflows/ci.yml +0 -37
  11. package/.github/workflows/publish.yml +0 -69
  12. package/.idea/apimo.js.iml +0 -13
  13. package/.idea/copilotDiffState.xml +0 -43
  14. package/.idea/inspectionProfiles/Project_Default.xml +0 -6
  15. package/.idea/jsLinters/eslint.xml +0 -6
  16. package/.idea/modules.xml +0 -8
  17. package/.idea/prettier.xml +0 -6
  18. package/.idea/vcs.xml +0 -6
  19. package/eslint.config.mjs +0 -3
  20. package/src/consts/catalogs.ts +0 -55
  21. package/src/consts/languages.ts +0 -22
  22. package/src/core/api.test.ts +0 -308
  23. package/src/core/api.ts +0 -230
  24. package/src/core/converters.ts +0 -7
  25. package/src/schemas/agency.ts +0 -66
  26. package/src/schemas/common.ts +0 -67
  27. package/src/schemas/internal.ts +0 -13
  28. package/src/schemas/property.ts +0 -257
  29. package/src/services/storage/dummy.cache.test.ts +0 -110
  30. package/src/services/storage/dummy.cache.ts +0 -21
  31. package/src/services/storage/filesystem.cache.test.ts +0 -243
  32. package/src/services/storage/filesystem.cache.ts +0 -94
  33. package/src/services/storage/memory.cache.test.ts +0 -94
  34. package/src/services/storage/memory.cache.ts +0 -69
  35. package/src/services/storage/types.ts +0 -20
  36. package/src/types/index.ts +0 -5
  37. package/src/utils/url.test.ts +0 -21
  38. package/src/utils/url.ts +0 -27
  39. package/tsconfig.json +0 -13
  40. package/vitest.config.ts +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apimo.js",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "A wrapper for the Apimo API with catalog caching for building custom Real Estate website using their technologies.",
5
5
  "author": "Vitaly Lysen <vitaly@lysen.dev> (https://lysen.dev)",
6
6
  "license": "MIT",
@@ -17,7 +17,21 @@
17
17
  "apimo",
18
18
  "real-estate"
19
19
  ],
20
- "main": "/dist/index.js",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "require": "./dist/index.js"
25
+ }
26
+ },
27
+ "main": "./dist/index.js",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "files": [
31
+ "LICENSE",
32
+ "README.md",
33
+ "dist/**/*"
34
+ ],
21
35
  "scripts": {
22
36
  "dev": "",
23
37
  "build": "tsc",
@@ -1,37 +0,0 @@
1
- name: Continuous Integration
2
-
3
- on:
4
- push:
5
- branches: [main, develop]
6
- pull_request:
7
- branches: [main, develop]
8
-
9
- jobs:
10
- test-and-lint:
11
- runs-on: ubuntu-latest
12
-
13
- strategy:
14
- matrix:
15
- node-version: [20, 22, 24]
16
-
17
- steps:
18
- - name: Checkout code
19
- uses: actions/checkout@v4
20
-
21
- - name: Setup Node.js ${{ matrix.node-version }}
22
- uses: actions/setup-node@v4
23
- with:
24
- node-version: ${{ matrix.node-version }}
25
- cache: yarn
26
-
27
- - name: Install dependencies
28
- run: yarn install --frozen-lockfile
29
-
30
- - name: Run linting
31
- run: yarn lint
32
-
33
- - name: Run tests
34
- run: yarn test
35
-
36
- - name: Run test coverage
37
- run: yarn test-coverage
@@ -1,69 +0,0 @@
1
- name: Build, Test & Publish to NPM
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'v*'
7
- workflow_dispatch:
8
-
9
- jobs:
10
- test:
11
- runs-on: ubuntu-latest
12
-
13
- steps:
14
- - name: Checkout code
15
- uses: actions/checkout@v4
16
-
17
- - name: Setup Node.js
18
- uses: actions/setup-node@v4
19
- with:
20
- node-version: '22'
21
- cache: yarn
22
-
23
- - name: Install dependencies
24
- run: yarn install --frozen-lockfile
25
-
26
- - name: Run linting
27
- run: yarn lint
28
-
29
- - name: Run tests
30
- run: yarn test
31
-
32
- - name: Run test coverage
33
- run: yarn test-coverage
34
-
35
- build-and-publish:
36
- needs: test
37
- runs-on: ubuntu-latest
38
-
39
- environment: Deploy
40
-
41
- steps:
42
- - name: Checkout code
43
- uses: actions/checkout@v4
44
-
45
- - name: Setup Node.js
46
- uses: actions/setup-node@v4
47
- with:
48
- node-version: '22'
49
- cache: yarn
50
- registry-url: 'https://registry.npmjs.org'
51
-
52
- - name: Install dependencies
53
- run: yarn install --frozen-lockfile
54
-
55
- - name: Build package
56
- run: yarn build
57
-
58
- - name: Check if dist directory exists
59
- run: |
60
- if [ ! -d "dist" ]; then
61
- echo "Build failed: dist directory not found"
62
- exit 1
63
- fi
64
- echo "Build successful: dist directory created"
65
-
66
- - name: Publish to NPM
67
- run: npm publish
68
- env:
69
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -1,13 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <module type="WEB_MODULE" version="4">
3
- <component name="NewModuleRootManager">
4
- <content url="file://$MODULE_DIR$">
5
- <excludeFolder url="file://$MODULE_DIR$/.tmp" />
6
- <excludeFolder url="file://$MODULE_DIR$/temp" />
7
- <excludeFolder url="file://$MODULE_DIR$/tmp" />
8
- <excludeFolder url="file://$MODULE_DIR$/dist" />
9
- </content>
10
- <orderEntry type="inheritedJdk" />
11
- <orderEntry type="sourceFolder" forTests="false" />
12
- </component>
13
- </module>
@@ -1,43 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="CopilotDiffPersistence">
4
- <option name="pendingDiffs">
5
- <map>
6
- <entry key="$PROJECT_DIR$/.github/workflows/ci.yml">
7
- <value>
8
- <PendingDiffInfo>
9
- <option name="filePath" value="$PROJECT_DIR$/.github/workflows/ci.yml" />
10
- <option name="updatedContent" value="name: Continuous Integration&#10;&#10;on:&#10; push:&#10; branches: [ main, develop ]&#10; pull_request:&#10; branches: [ main, develop ]&#10;&#10;jobs:&#10; test-and-lint:&#10; runs-on: ubuntu-latest&#10;&#10; strategy:&#10; matrix:&#10; node-version: [18, 20, 22]&#10;&#10; steps:&#10; - name: Checkout code&#10; uses: actions/checkout@v4&#10;&#10; - name: Setup Node.js ${{ matrix.node-version }}&#10; uses: actions/setup-node@v4&#10; with:&#10; node-version: ${{ matrix.node-version }}&#10; cache: 'yarn'&#10;&#10; - name: Install dependencies&#10; run: yarn install --frozen-lockfile&#10;&#10; - name: Run linting&#10; run: yarn lint&#10;&#10; - name: Run tests&#10; run: yarn test&#10;&#10; - name: Run test coverage&#10; run: yarn test-coverage&#10;&#10; - name: Upload coverage reports to Codecov&#10; uses: codecov/codecov-action@v4&#10; if: matrix.node-version == 20&#10; with:&#10; file: ./coverage/lcov.info&#10; fail_ci_if_error: false&#10; env:&#10; CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}" />
11
- </PendingDiffInfo>
12
- </value>
13
- </entry>
14
- <entry key="$PROJECT_DIR$/.github/workflows/publish.yml">
15
- <value>
16
- <PendingDiffInfo>
17
- <option name="filePath" value="$PROJECT_DIR$/.github/workflows/publish.yml" />
18
- <option name="updatedContent" value="name: Build, Test &amp; Publish to NPM&#10;&#10;on:&#10; push:&#10; tags:&#10; - 'v*' # Triggers on version tags like v1.0.0, v1.2.3, etc.&#10; workflow_dispatch: # Allows manual triggering&#10;&#10;jobs:&#10; test:&#10; runs-on: ubuntu-latest&#10; &#10; steps:&#10; - name: Checkout code&#10; uses: actions/checkout@v4&#10; &#10; - name: Setup Node.js&#10; uses: actions/setup-node@v4&#10; with:&#10; node-version: '18'&#10; cache: 'yarn'&#10; &#10; - name: Install dependencies&#10; run: yarn install --frozen-lockfile&#10; &#10; - name: Run linting&#10; run: yarn lint&#10; &#10; - name: Run tests&#10; run: yarn test&#10; &#10; - name: Run test coverage&#10; run: yarn test-coverage&#10;&#10; build-and-publish:&#10; needs: test&#10; runs-on: ubuntu-latest&#10; &#10; steps:&#10; - name: Checkout code&#10; uses: actions/checkout@v4&#10; &#10; - name: Setup Node.js&#10; uses: actions/setup-node@v4&#10; with:&#10; node-version: '18'&#10; cache: 'yarn'&#10; registry-url: 'https://registry.npmjs.org'&#10; &#10; - name: Install dependencies&#10; run: yarn install --frozen-lockfile&#10; &#10; - name: Build package&#10; run: yarn build&#10; &#10; - name: Check if dist directory exists&#10; run: |&#10; if [ ! -d &quot;dist&quot; ]; then&#10; echo &quot;Build failed: dist directory not found&quot;&#10; exit 1&#10; fi&#10; echo &quot;Build successful: dist directory created&quot;&#10; &#10; - name: Publish to NPM&#10; run: npm publish&#10; env:&#10; NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}" />
19
- </PendingDiffInfo>
20
- </value>
21
- </entry>
22
- <entry key="$PROJECT_DIR$/package.json">
23
- <value>
24
- <PendingDiffInfo>
25
- <option name="filePath" value="$PROJECT_DIR$/package.json" />
26
- <option name="originalContent" value="{&#10; &quot;name&quot;: &quot;apimo.js&quot;,&#10; &quot;version&quot;: &quot;1.0.1&quot;,&#10; &quot;description&quot;: &quot;A wrapper for the Apimo API with catalog caching for building custom Real Estate website using their technologies.&quot;,&#10; &quot;main&quot;: &quot;/dist/index.js&quot;,&#10; &quot;repository&quot;: {&#10; &quot;type&quot;: &quot;git&quot;,&#10; &quot;url&quot;: &quot;https://github.com/Neikow/apimo.js.git&quot;&#10; },&#10; &quot;homepage&quot;: &quot;https://github.com/Neikow/apimo.js&quot;,&#10; &quot;bugs&quot;: {&#10; &quot;url&quot;: &quot;https://github.com/Neikow/apimo.js/issues&quot;&#10; },&#10; &quot;keywords&quot;: [&#10; &quot;api&quot;,&#10; &quot;apimo&quot;,&#10; &quot;real-estate&quot;&#10; ],&#10; &quot;scripts&quot;: {&#10; &quot;dev&quot;: &quot;&quot;,&#10; &quot;build&quot;: &quot;tsc&quot;,&#10; &quot;run&quot;: &quot;node dist/index.js&quot;,&#10; &quot;test&quot;: &quot;vitest run&quot;,&#10; &quot;test-coverage&quot;: &quot;vitest run --coverage&quot;&#10; },&#10; &quot;author&quot;: &quot;Vitaly Lysen &lt;vitaly@lysen.dev&gt; (https://lysen.dev)&quot;,&#10; &quot;license&quot;: &quot;MIT&quot;,&#10; &quot;dependencies&quot;: {&#10; &quot;bottleneck&quot;: &quot;^2.19.5&quot;,&#10; &quot;merge-anything&quot;: &quot;^6.0.6&quot;,&#10; &quot;zod&quot;: &quot;^3.21.4&quot;&#10; },&#10; &quot;devDependencies&quot;: {&#10; &quot;@antfu/eslint-config&quot;: &quot;^5.0.0&quot;,&#10; &quot;@types/node&quot;: &quot;^24.1.0&quot;,&#10; &quot;@anatine/zod-mock&quot;: &quot;^3.14.0&quot;,&#10; &quot;@faker-js/faker&quot;: &quot;^9.9.0&quot;,&#10; &quot;@vitest/coverage-v8&quot;: &quot;^3.2.4&quot;,&#10; &quot;dotenv&quot;: &quot;^17.2.1&quot;,&#10; &quot;eslint&quot;: &quot;^9.32.0&quot;,&#10; &quot;typescript&quot;: &quot;^5.9.2&quot;,&#10; &quot;vitest&quot;: &quot;^3.2.4&quot;&#10; }&#10;}&#10;" />
27
- <option name="updatedContent" value="{&#10; &quot;name&quot;: &quot;apimo.js&quot;,&#10; &quot;version&quot;: &quot;1.0.1&quot;,&#10; &quot;description&quot;: &quot;A wrapper for the Apimo API with catalog caching for building custom Real Estate website using their technologies.&quot;,&#10; &quot;main&quot;: &quot;/dist/index.js&quot;,&#10; &quot;repository&quot;: {&#10; &quot;type&quot;: &quot;git&quot;,&#10; &quot;url&quot;: &quot;https://github.com/Neikow/apimo.js.git&quot;&#10; },&#10; &quot;homepage&quot;: &quot;https://github.com/Neikow/apimo.js&quot;,&#10; &quot;bugs&quot;: {&#10; &quot;url&quot;: &quot;https://github.com/Neikow/apimo.js/issues&quot;&#10; },&#10; &quot;keywords&quot;: [&#10; &quot;api&quot;,&#10; &quot;apimo&quot;,&#10; &quot;real-estate&quot;&#10; ],&#10; &quot;scripts&quot;: {&#10; &quot;dev&quot;: &quot;&quot;,&#10; &quot;build&quot;: &quot;tsc&quot;,&#10; &quot;run&quot;: &quot;node dist/index.js&quot;,&#10; &quot;test&quot;: &quot;vitest run&quot;,&#10; &quot;test-coverage&quot;: &quot;vitest run --coverage&quot;,&#10; &quot;lint&quot;: &quot;eslint .&quot;&#10; },&#10; &quot;author&quot;: &quot;Vitaly Lysen &lt;vitaly@lysen.dev&gt; (https://lysen.dev)&quot;,&#10; &quot;license&quot;: &quot;MIT&quot;,&#10; &quot;dependencies&quot;: {&#10; &quot;bottleneck&quot;: &quot;^2.19.5&quot;,&#10; &quot;merge-anything&quot;: &quot;^6.0.6&quot;,&#10; &quot;zod&quot;: &quot;^3.21.4&quot;&#10; },&#10; &quot;devDependencies&quot;: {&#10; &quot;@antfu/eslint-config&quot;: &quot;^5.0.0&quot;,&#10; &quot;@types/node&quot;: &quot;^24.1.0&quot;,&#10; &quot;@anatine/zod-mock&quot;: &quot;^3.14.0&quot;,&#10; &quot;@faker-js/faker&quot;: &quot;^9.9.0&quot;,&#10; &quot;@vitest/coverage-v8&quot;: &quot;^3.2.4&quot;,&#10; &quot;dotenv&quot;: &quot;^17.2.1&quot;,&#10; &quot;eslint&quot;: &quot;^9.32.0&quot;,&#10; &quot;typescript&quot;: &quot;^5.9.2&quot;,&#10; &quot;vitest&quot;: &quot;^3.2.4&quot;&#10; }&#10;}" />
28
- </PendingDiffInfo>
29
- </value>
30
- </entry>
31
- <entry key="$PROJECT_DIR$/src/services/storage/dummy.cache.test.ts">
32
- <value>
33
- <PendingDiffInfo>
34
- <option name="filePath" value="$PROJECT_DIR$/src/services/storage/dummy.cache.test.ts" />
35
- <option name="originalContent" value="import type { CatalogName } from '../../consts/catalogs'&#10;import type { ApiCulture } from '../../consts/languages'&#10;import { afterEach, beforeEach, describe, expect, it } from 'vitest'&#10;import { DummyCache } from './dummy.cache'&#10;import { CacheExpiredError } from './types'&#10;&#10;describe('cache - Dummy', () =&gt; {&#10; let cache: DummyCache&#10;&#10; beforeEach(() =&gt; {&#10; cache = new DummyCache()&#10; })&#10;&#10; afterEach(() =&gt; {&#10; // No cleanup needed for dummy cache&#10; })&#10;&#10; describe('constructor', () =&gt; {&#10; it('should create an instance without any configuration', () =&gt; {&#10; const dummyCache = new DummyCache()&#10; expect(dummyCache).toBeInstanceOf(DummyCache)&#10; })&#10; })&#10;&#10; describe('setEntries', () =&gt; {&#10; const culture: ApiCulture = 'en'&#10; const entries = [&#10; { id: 1, name: 'Item 1', name_plurial: 'Items 1' },&#10; { id: 2, name: 'Item 2', name_plurial: 'Items 2' },&#10; ]&#10;&#10; it('should not throw when setting entries', async () =&gt; {&#10; const catalogName: CatalogName = 'book_step'&#10; await expect(cache.setEntries(catalogName, culture, entries)).resolves.toBeUndefined()&#10; })&#10;&#10; it('should handle empty entries array', async () =&gt; {&#10; const catalogName: CatalogName = 'book_step'&#10; await expect(cache.setEntries(catalogName, culture, [])).resolves.toBeUndefined()&#10; })&#10;&#10; it('should handle different catalog and culture combinations', async () =&gt; {&#10; await expect(cache.setEntries('book_step', 'en', entries)).resolves.toBeUndefined()&#10; await expect(cache.setEntries('property_land', 'fr', entries)).resolves.toBeUndefined()&#10; await expect(cache.setEntries('property_type', 'de', entries)).resolves.toBeUndefined()&#10; })&#10; })&#10;&#10; describe('getEntry', () =&gt; {&#10; const culture: ApiCulture = 'en'&#10;&#10; it('should always throw CacheExpiredError regardless of parameters', async () =&gt; {&#10; const catalogName: CatalogName = 'book_step'&#10; await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError)&#10; })&#10;&#10; it('should throw CacheExpiredError for any ID', async () =&gt; {&#10; const catalogName: CatalogName = 'book_step'&#10; await expect(cache.getEntry(catalogName, culture, 999)).rejects.toThrow(CacheExpiredError)&#10; await expect(cache.getEntry(catalogName, culture, 0)).rejects.toThrow(CacheExpiredError)&#10; await expect(cache.getEntry(catalogName, culture, -1)).rejects.toThrow(CacheExpiredError)&#10; })&#10;&#10; it('should throw CacheExpiredError for different catalogs and cultures', async () =&gt; {&#10; await expect(cache.getEntry('book_step', 'en', 1)).rejects.toThrow(CacheExpiredError)&#10; await expect(cache.getEntry('property_land', 'fr', 1)).rejects.toThrow(CacheExpiredError)&#10; await expect(cache.getEntry('property_type', 'de', 1)).rejects.toThrow(CacheExpiredError)&#10; })&#10;&#10; it('should throw CacheExpiredError even after setting entries', async () =&gt; {&#10; const catalogName: CatalogName = 'book_step'&#10; const entries = [{ id: 1, name: 'Item 1', name_plurial: 'Items 1' }]&#10;&#10; await cache.setEntries(catalogName, culture, entries)&#10; await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError)&#10; })&#10; })&#10;})&#10;" />
36
- <option name="updatedContent" value="import type { CatalogName } from '../../consts/catalogs'&#10;import type { ApiCulture } from '../../consts/languages'&#10;import { afterEach, beforeEach, describe, expect, it } from 'vitest'&#10;import { DummyCache } from './dummy.cache'&#10;import { CacheExpiredError } from './types'&#10;&#10;describe('cache - Dummy', () =&gt; {&#10; let cache: DummyCache&#10;&#10; beforeEach(() =&gt; {&#10; cache = new DummyCache()&#10; })&#10;&#10; afterEach(() =&gt; {&#10; // No cleanup needed for dummy cache&#10; })&#10;&#10; describe('constructor', () =&gt; {&#10; it('should create an instance without any configuration', () =&gt; {&#10; const dummyCache = new DummyCache()&#10; expect(dummyCache).toBeInstanceOf(DummyCache)&#10; })&#10; })&#10;&#10; describe('setEntries', () =&gt; {&#10; const culture: ApiCulture = 'en'&#10; const entries = [&#10; { id: 1, name: 'Item 1', name_plurial: 'Items 1' },&#10; { id: 2, name: 'Item 2', name_plurial: 'Items 2' },&#10; ]&#10;&#10; it('should not throw when setting entries', async () =&gt; {&#10; const catalogName: CatalogName = 'book_step'&#10; await expect(cache.setEntries(catalogName, culture, entries)).resolves.toBeUndefined()&#10; })&#10;&#10; it('should handle empty entries array', async () =&gt; {&#10; const catalogName: CatalogName = 'book_step'&#10; await expect(cache.setEntries(catalogName, culture, [])).resolves.toBeUndefined()&#10; })&#10;&#10; it('should handle different catalog and culture combinations', async () =&gt; {&#10; await expect(cache.setEntries('book_step', 'en', entries)).resolves.toBeUndefined()&#10; await expect(cache.setEntries('property_land', 'fr', entries)).resolves.toBeUndefined()&#10; await expect(cache.setEntries('property_type', 'de', entries)).resolves.toBeUndefined()&#10; })&#10; })&#10;&#10; describe('getEntry', () =&gt; {&#10; const culture: ApiCulture = 'en'&#10;&#10; it('should always throw CacheExpiredError regardless of parameters', async () =&gt; {&#10; const catalogName: CatalogName = 'book_step'&#10; await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError)&#10; })&#10;&#10; it('should throw CacheExpiredError for any ID', async () =&gt; {&#10; const catalogName: CatalogName = 'book_step'&#10; await expect(cache.getEntry(catalogName, culture, 999)).rejects.toThrow(CacheExpiredError)&#10; await expect(cache.getEntry(catalogName, culture, 0)).rejects.toThrow(CacheExpiredError)&#10; await expect(cache.getEntry(catalogName, culture, -1)).rejects.toThrow(CacheExpiredError)&#10; })&#10;&#10; it('should throw CacheExpiredError for different catalogs and cultures', async () =&gt; {&#10; await expect(cache.getEntry('book_step', 'en', 1)).rejects.toThrow(CacheExpiredError)&#10; await expect(cache.getEntry('property_land', 'fr', 1)).rejects.toThrow(CacheExpiredError)&#10; await expect(cache.getEntry('property_type', 'de', 1)).rejects.toThrow(CacheExpiredError)&#10; })&#10;&#10; it('should throw CacheExpiredError even after setting entries', async () =&gt; {&#10; const catalogName: CatalogName = 'book_step'&#10; const entries = [{ id: 1, name: 'Item 1', name_plurial: 'Items 1' }]&#10;&#10; await cache.setEntries(catalogName, culture, entries)&#10; await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError)&#10; })&#10; })&#10;&#10; describe('behavior consistency', () =&gt; {&#10; it('should behave consistently across multiple calls', async () =&gt; {&#10; const catalogName: CatalogName = 'book_step'&#10; const culture: ApiCulture = 'en'&#10; const entries = [{ id: 1, name: 'Item 1', name_plurial: 'Items 1' }]&#10;&#10; // Multiple setEntries calls should not throw&#10; await expect(cache.setEntries(catalogName, culture, entries)).resolves.toBeUndefined()&#10; await expect(cache.setEntries(catalogName, culture, entries)).resolves.toBeUndefined()&#10;&#10; // Multiple getEntry calls should always throw&#10; await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError)&#10; await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError)&#10; })&#10;&#10; it('should maintain dummy behavior regardless of cache state', async () =&gt; {&#10; const catalogName: CatalogName = 'book_step'&#10; const culture: ApiCulture = 'en'&#10;&#10; // Should throw before any operations&#10; await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError)&#10;&#10; // Should still throw after setting entries&#10; await cache.setEntries(catalogName, culture, [{ id: 1, name: 'Test', name_plurial: 'Tests' }])&#10; await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError)&#10;&#10; // Should still throw after multiple operations&#10; await cache.setEntries(catalogName, culture, [])&#10; await expect(cache.getEntry(catalogName, culture, 999)).rejects.toThrow(CacheExpiredError)&#10; })&#10; })&#10;})" />
37
- </PendingDiffInfo>
38
- </value>
39
- </entry>
40
- </map>
41
- </option>
42
- </component>
43
- </project>
@@ -1,6 +0,0 @@
1
- <component name="InspectionProjectProfileManager">
2
- <profile version="1.0">
3
- <option name="myName" value="Project Default" />
4
- <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
5
- </profile>
6
- </component>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="EslintConfiguration">
4
- <option name="fix-on-save" value="true" />
5
- </component>
6
- </project>
package/.idea/modules.xml DELETED
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectModuleManager">
4
- <modules>
5
- <module fileurl="file://$PROJECT_DIR$/.idea/apimo.js.iml" filepath="$PROJECT_DIR$/.idea/apimo.js.iml" />
6
- </modules>
7
- </component>
8
- </project>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="PrettierConfiguration">
4
- <option name="myConfigurationMode" value="AUTOMATIC" />
5
- </component>
6
- </project>
package/.idea/vcs.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
- </component>
6
- </project>
package/eslint.config.mjs DELETED
@@ -1,3 +0,0 @@
1
- import antfu from '@antfu/eslint-config'
2
-
3
- export default antfu()
@@ -1,55 +0,0 @@
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]
@@ -1,22 +0,0 @@
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]
@@ -1,308 +0,0 @@
1
- import type { MockedFunction } from 'vitest'
2
- import type { CatalogName } from '../consts/catalogs'
3
- import type { ApiCulture } from '../consts/languages'
4
- import { afterEach, beforeEach, it as defaultIt, describe, expect, vi } from 'vitest'
5
- import { z } from 'zod'
6
- import { DummyCache } from '../services/storage/dummy.cache'
7
- import { MemoryCache } from '../services/storage/memory.cache'
8
- import { Api, DEFAULT_BASE_URL } from './api'
9
-
10
- // Mock fetch globally
11
- const mockFetch = vi.fn() as MockedFunction<typeof fetch>
12
-
13
- interface ResponseMockerConfig {
14
- ok?: boolean
15
- status?: number
16
- json?: () => any
17
- }
18
-
19
- type ResponseMocker = (config?: ResponseMockerConfig) => void
20
-
21
- const PROVIDER = '0'
22
- const TOKEN = 'TOKEN'
23
-
24
- const BasicAuthHeaders = {
25
- Authorization: `Basic ${btoa(`${PROVIDER}:${TOKEN}`)}`,
26
- }
27
-
28
- const it = defaultIt.extend<{
29
- api: Api
30
- mockResponse: ResponseMocker
31
- }>({
32
- // eslint-disable-next-line no-empty-pattern
33
- api: async ({}, use) => {
34
- let api: Api | null = new Api('0', 'TOKEN', {
35
- catalogs: {
36
- transform: {
37
- active: false,
38
- },
39
- },
40
- })
41
- await use(api)
42
- api = null
43
- },
44
- // eslint-disable-next-line no-empty-pattern
45
- mockResponse: async ({}, use) => {
46
- const mockResponse: ResponseMocker = (config) => {
47
- mockFetch.mockResolvedValue({
48
- ok: config?.ok ?? true,
49
- status: config?.status ?? 200,
50
- json: config?.json ? vi.fn().mockResolvedValue(config.json()) : vi.fn().mockResolvedValue({}),
51
- text: vi.fn(),
52
- headers: new Headers(),
53
- statusText: 'OK',
54
- url: '',
55
- redirected: false,
56
- type: 'basic',
57
- body: null,
58
- bodyUsed: false,
59
- clone: vi.fn(),
60
- arrayBuffer: vi.fn(),
61
- blob: vi.fn(),
62
- formData: vi.fn(),
63
- } as unknown as Response)
64
- }
65
- await use(mockResponse)
66
- },
67
- })
68
-
69
- describe('api', () => {
70
- let mockResponse: Response
71
-
72
- beforeEach(() => {
73
- // Mock global fetch
74
- vi.stubGlobal('fetch', mockFetch)
75
-
76
- // Create a mock response object
77
- mockResponse = {
78
- ok: true,
79
- status: 200,
80
- json: vi.fn(),
81
- text: vi.fn(),
82
- headers: new Headers(),
83
- statusText: 'OK',
84
- url: '',
85
- redirected: false,
86
- type: 'basic',
87
- body: null,
88
- bodyUsed: false,
89
- clone: vi.fn(),
90
- arrayBuffer: vi.fn(),
91
- blob: vi.fn(),
92
- formData: vi.fn(),
93
- } as unknown as Response
94
-
95
- mockFetch.mockResolvedValue(mockResponse)
96
- })
97
-
98
- afterEach(() => {
99
- vi.unstubAllGlobals()
100
- vi.clearAllMocks()
101
- })
102
-
103
- describe('constructor', () => {
104
- it('should accept a provider, a token and a base config', ({ api }) => {
105
- expect(api).toBeInstanceOf(Api)
106
- })
107
-
108
- it('should use default config when no additional config provided', ({ api }) => {
109
- expect(api.config).toStrictEqual({
110
- baseUrl: DEFAULT_BASE_URL,
111
- culture: 'en' as ApiCulture,
112
- catalogs: {
113
- cache: {
114
- active: true,
115
- adapter: expect.any(MemoryCache),
116
- },
117
- transform: {
118
- active: false,
119
- },
120
- },
121
- })
122
- })
123
-
124
- it('should merge custom config with defaults', () => {
125
- const testApi = new Api('provider', 'token', {
126
- baseUrl: 'https://custom.api.com',
127
- culture: 'fr' as ApiCulture,
128
- catalogs: {
129
- cache: {
130
- active: false,
131
- adapter: new DummyCache(),
132
- },
133
- },
134
- })
135
-
136
- expect(testApi.config).toStrictEqual({
137
- baseUrl: 'https://custom.api.com',
138
- culture: 'fr',
139
- catalogs: {
140
- cache: {
141
- active: false,
142
- adapter: expect.any(DummyCache),
143
- },
144
- transform: {
145
- active: true,
146
- },
147
- },
148
- })
149
- })
150
-
151
- it('should use provided cache adapter', () => {
152
- const testApi = new Api('provider', 'token', {
153
- catalogs: {
154
- cache: {
155
- adapter: new DummyCache(),
156
- },
157
- },
158
- })
159
- expect(testApi.cache).toBeInstanceOf(DummyCache)
160
- })
161
-
162
- it('should use DummyCache when cache is not active', () => {
163
- const testApi = new Api('provider', 'token', {
164
- catalogs: {
165
- cache: {
166
- active: false,
167
- adapter: new MemoryCache(),
168
- },
169
- },
170
- })
171
- expect(testApi.cache).toBeInstanceOf(DummyCache)
172
- })
173
- })
174
-
175
- describe('fetch', () => {
176
- it('should have the right authorization headers when fetching', async () => {
177
- const testApi = new Api('provider', 'token')
178
- await testApi.fetch(DEFAULT_BASE_URL)
179
-
180
- expect(mockFetch).toHaveBeenCalledExactlyOnceWith(
181
- DEFAULT_BASE_URL,
182
- {
183
- headers: {
184
- Authorization: `Basic ${btoa('provider:token')}`,
185
- },
186
- },
187
- )
188
- })
189
-
190
- it('should merge additional headers with authorization', async ({ api }) => {
191
- const customHeaders = { 'Content-Type': 'application/json' }
192
- await api.fetch(DEFAULT_BASE_URL, { headers: customHeaders })
193
-
194
- expect(mockFetch).toHaveBeenCalledWith(
195
- DEFAULT_BASE_URL,
196
- {
197
- headers: {
198
- ...BasicAuthHeaders,
199
- 'Content-Type': 'application/json',
200
- },
201
- },
202
- )
203
- })
204
-
205
- it('should pass through other fetch options', async ({ api }) => {
206
- const options = {
207
- method: 'POST',
208
- body: JSON.stringify({ test: 'data' }),
209
- headers: { 'Custom-Header': 'value' },
210
- }
211
-
212
- await api.fetch(DEFAULT_BASE_URL, options)
213
-
214
- expect(mockFetch).toHaveBeenCalledWith(
215
- DEFAULT_BASE_URL,
216
- {
217
- method: 'POST',
218
- body: JSON.stringify({ test: 'data' }),
219
- headers: {
220
- ...BasicAuthHeaders,
221
- 'Custom-Header': 'value',
222
- },
223
- },
224
- )
225
- })
226
-
227
- it('should handle rate limiting with Bottleneck', async ({ api }) => {
228
- // Make multiple concurrent requests to test rate limiting
229
- const promises = Array.from({ length: 3 }, () => api.fetch(DEFAULT_BASE_URL))
230
- await Promise.all(promises)
231
-
232
- expect(mockFetch).toHaveBeenCalledTimes(3)
233
- })
234
- })
235
-
236
- describe('get', () => {
237
- it('should fetch and parse according to the specified schema', async ({ mockResponse, api }) => {
238
- mockResponse({
239
- json: () => ({ success: true }),
240
- })
241
-
242
- const spy = vi.spyOn(api, 'fetch')
243
- await api.get(['path', 'to', 'catalogs'], z.object({ success: z.boolean() }), { culture: 'en' })
244
- expect(spy).toHaveBeenCalledExactlyOnceWith(
245
- new URL('https://api.apimo.pro/path/to/catalogs?culture=en'),
246
- )
247
- })
248
- })
249
-
250
- describe('populateCache', () => {
251
- it('should populate cache without returning entry when no id provided', async ({ api, mockResponse }) => {
252
- const catalogName: CatalogName = 'property_type'
253
- const culture: ApiCulture = 'en'
254
- const mockEntries = [
255
- { id: 1, name: 'Apartment', name_plurial: 'Apartments' },
256
- { id: 2, name: 'House', name_plurial: 'Houses' },
257
- ]
258
-
259
- mockResponse({
260
- json: () => mockEntries,
261
- })
262
-
263
- const result = await api.populateCache(catalogName, culture)
264
-
265
- expect(result).toBeUndefined()
266
- expect(mockFetch).toHaveBeenCalledExactlyOnceWith(
267
- new URL(`https://api.apimo.pro/catalogs/${catalogName}?culture=${culture}`),
268
- {
269
- headers: BasicAuthHeaders,
270
- },
271
- )
272
- })
273
-
274
- it('should populate cache and return specific entry when id provided', async ({ api, mockResponse }) => {
275
- const catalogName: CatalogName = 'property_type'
276
- const culture: ApiCulture = 'en'
277
- const mockEntries = [
278
- { id: 1, name: 'Apartment', name_plurial: 'Apartments' },
279
- { id: 2, name: 'House', name_plurial: 'Houses' },
280
- ]
281
-
282
- mockResponse({
283
- json: () => mockEntries,
284
- })
285
-
286
- const result = await api.populateCache(catalogName, culture, 1)
287
-
288
- expect(result).toEqual({
289
- name: 'Apartment',
290
- namePlural: 'Apartments',
291
- })
292
- })
293
-
294
- it('should return null when requested id not found', async ({ api, mockResponse }) => {
295
- const catalogName: CatalogName = 'property_type'
296
- const culture: ApiCulture = 'en'
297
- const mockEntries = [
298
- { id: 1, name: 'Apartment', name_plurial: 'Apartments' },
299
- ]
300
-
301
- mockResponse({ json: () => mockEntries })
302
-
303
- const result = await api.populateCache(catalogName, culture, 999)
304
-
305
- expect(result).toBeNull()
306
- })
307
- })
308
- })