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.
- package/README.md +314 -48
- package/dist/src/core/api.d.ts +1 -1
- package/dist/src/core/api.js +1 -1
- package/dist/src/core/api.test.js +7 -7
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.js +10 -0
- package/dist/src/schemas/agency.d.ts +8 -8
- package/dist/src/schemas/property.d.ts +151 -151
- package/package.json +16 -2
- package/.github/workflows/ci.yml +0 -37
- package/.github/workflows/publish.yml +0 -69
- package/.idea/apimo.js.iml +0 -13
- package/.idea/copilotDiffState.xml +0 -43
- package/.idea/inspectionProfiles/Project_Default.xml +0 -6
- package/.idea/jsLinters/eslint.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/prettier.xml +0 -6
- package/.idea/vcs.xml +0 -6
- package/eslint.config.mjs +0 -3
- package/src/consts/catalogs.ts +0 -55
- package/src/consts/languages.ts +0 -22
- package/src/core/api.test.ts +0 -308
- package/src/core/api.ts +0 -230
- package/src/core/converters.ts +0 -7
- package/src/schemas/agency.ts +0 -66
- package/src/schemas/common.ts +0 -67
- package/src/schemas/internal.ts +0 -13
- package/src/schemas/property.ts +0 -257
- package/src/services/storage/dummy.cache.test.ts +0 -110
- package/src/services/storage/dummy.cache.ts +0 -21
- package/src/services/storage/filesystem.cache.test.ts +0 -243
- package/src/services/storage/filesystem.cache.ts +0 -94
- package/src/services/storage/memory.cache.test.ts +0 -94
- package/src/services/storage/memory.cache.ts +0 -69
- package/src/services/storage/types.ts +0 -20
- package/src/types/index.ts +0 -5
- package/src/utils/url.test.ts +0 -21
- package/src/utils/url.ts +0 -27
- package/tsconfig.json +0 -13
- 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.
|
|
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
|
-
"
|
|
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",
|
package/.github/workflows/ci.yml
DELETED
|
@@ -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 }}
|
package/.idea/apimo.js.iml
DELETED
|
@@ -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 on: push: branches: [ main, develop ] pull_request: branches: [ main, develop ] jobs: test-and-lint: runs-on: ubuntu-latest strategy: matrix: node-version: [18, 20, 22] steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'yarn' - name: Install dependencies run: yarn install --frozen-lockfile - name: Run linting run: yarn lint - name: Run tests run: yarn test - name: Run test coverage run: yarn test-coverage - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4 if: matrix.node-version == 20 with: file: ./coverage/lcov.info fail_ci_if_error: false env: 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 & Publish to NPM on: push: tags: - 'v*' # Triggers on version tags like v1.0.0, v1.2.3, etc. workflow_dispatch: # Allows manual triggering jobs: test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' cache: 'yarn' - name: Install dependencies run: yarn install --frozen-lockfile - name: Run linting run: yarn lint - name: Run tests run: yarn test - name: Run test coverage run: yarn test-coverage build-and-publish: needs: test runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' cache: 'yarn' registry-url: 'https://registry.npmjs.org' - name: Install dependencies run: yarn install --frozen-lockfile - name: Build package run: yarn build - name: Check if dist directory exists run: | if [ ! -d "dist" ]; then echo "Build failed: dist directory not found" exit 1 fi echo "Build successful: dist directory created" - name: Publish to NPM run: npm publish env: 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="{ "name": "apimo.js", "version": "1.0.1", "description": "A wrapper for the Apimo API with catalog caching for building custom Real Estate website using their technologies.", "main": "/dist/index.js", "repository": { "type": "git", "url": "https://github.com/Neikow/apimo.js.git" }, "homepage": "https://github.com/Neikow/apimo.js", "bugs": { "url": "https://github.com/Neikow/apimo.js/issues" }, "keywords": [ "api", "apimo", "real-estate" ], "scripts": { "dev": "", "build": "tsc", "run": "node dist/index.js", "test": "vitest run", "test-coverage": "vitest run --coverage" }, "author": "Vitaly Lysen <vitaly@lysen.dev> (https://lysen.dev)", "license": "MIT", "dependencies": { "bottleneck": "^2.19.5", "merge-anything": "^6.0.6", "zod": "^3.21.4" }, "devDependencies": { "@antfu/eslint-config": "^5.0.0", "@types/node": "^24.1.0", "@anatine/zod-mock": "^3.14.0", "@faker-js/faker": "^9.9.0", "@vitest/coverage-v8": "^3.2.4", "dotenv": "^17.2.1", "eslint": "^9.32.0", "typescript": "^5.9.2", "vitest": "^3.2.4" } } " />
|
|
27
|
-
<option name="updatedContent" value="{ "name": "apimo.js", "version": "1.0.1", "description": "A wrapper for the Apimo API with catalog caching for building custom Real Estate website using their technologies.", "main": "/dist/index.js", "repository": { "type": "git", "url": "https://github.com/Neikow/apimo.js.git" }, "homepage": "https://github.com/Neikow/apimo.js", "bugs": { "url": "https://github.com/Neikow/apimo.js/issues" }, "keywords": [ "api", "apimo", "real-estate" ], "scripts": { "dev": "", "build": "tsc", "run": "node dist/index.js", "test": "vitest run", "test-coverage": "vitest run --coverage", "lint": "eslint ." }, "author": "Vitaly Lysen <vitaly@lysen.dev> (https://lysen.dev)", "license": "MIT", "dependencies": { "bottleneck": "^2.19.5", "merge-anything": "^6.0.6", "zod": "^3.21.4" }, "devDependencies": { "@antfu/eslint-config": "^5.0.0", "@types/node": "^24.1.0", "@anatine/zod-mock": "^3.14.0", "@faker-js/faker": "^9.9.0", "@vitest/coverage-v8": "^3.2.4", "dotenv": "^17.2.1", "eslint": "^9.32.0", "typescript": "^5.9.2", "vitest": "^3.2.4" } }" />
|
|
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' import type { ApiCulture } from '../../consts/languages' import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { DummyCache } from './dummy.cache' import { CacheExpiredError } from './types' describe('cache - Dummy', () => { let cache: DummyCache beforeEach(() => { cache = new DummyCache() }) afterEach(() => { // No cleanup needed for dummy cache }) describe('constructor', () => { it('should create an instance without any configuration', () => { const dummyCache = new DummyCache() expect(dummyCache).toBeInstanceOf(DummyCache) }) }) describe('setEntries', () => { const culture: ApiCulture = 'en' const entries = [ { id: 1, name: 'Item 1', name_plurial: 'Items 1' }, { id: 2, name: 'Item 2', name_plurial: 'Items 2' }, ] it('should not throw when setting entries', async () => { const catalogName: CatalogName = 'book_step' await expect(cache.setEntries(catalogName, culture, entries)).resolves.toBeUndefined() }) it('should handle empty entries array', async () => { const catalogName: CatalogName = 'book_step' await expect(cache.setEntries(catalogName, culture, [])).resolves.toBeUndefined() }) it('should handle different catalog and culture combinations', async () => { await expect(cache.setEntries('book_step', 'en', entries)).resolves.toBeUndefined() await expect(cache.setEntries('property_land', 'fr', entries)).resolves.toBeUndefined() await expect(cache.setEntries('property_type', 'de', entries)).resolves.toBeUndefined() }) }) describe('getEntry', () => { const culture: ApiCulture = 'en' it('should always throw CacheExpiredError regardless of parameters', async () => { const catalogName: CatalogName = 'book_step' await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError) }) it('should throw CacheExpiredError for any ID', async () => { const catalogName: CatalogName = 'book_step' await expect(cache.getEntry(catalogName, culture, 999)).rejects.toThrow(CacheExpiredError) await expect(cache.getEntry(catalogName, culture, 0)).rejects.toThrow(CacheExpiredError) await expect(cache.getEntry(catalogName, culture, -1)).rejects.toThrow(CacheExpiredError) }) it('should throw CacheExpiredError for different catalogs and cultures', async () => { await expect(cache.getEntry('book_step', 'en', 1)).rejects.toThrow(CacheExpiredError) await expect(cache.getEntry('property_land', 'fr', 1)).rejects.toThrow(CacheExpiredError) await expect(cache.getEntry('property_type', 'de', 1)).rejects.toThrow(CacheExpiredError) }) it('should throw CacheExpiredError even after setting entries', async () => { const catalogName: CatalogName = 'book_step' const entries = [{ id: 1, name: 'Item 1', name_plurial: 'Items 1' }] await cache.setEntries(catalogName, culture, entries) await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError) }) }) }) " />
|
|
36
|
-
<option name="updatedContent" value="import type { CatalogName } from '../../consts/catalogs' import type { ApiCulture } from '../../consts/languages' import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { DummyCache } from './dummy.cache' import { CacheExpiredError } from './types' describe('cache - Dummy', () => { let cache: DummyCache beforeEach(() => { cache = new DummyCache() }) afterEach(() => { // No cleanup needed for dummy cache }) describe('constructor', () => { it('should create an instance without any configuration', () => { const dummyCache = new DummyCache() expect(dummyCache).toBeInstanceOf(DummyCache) }) }) describe('setEntries', () => { const culture: ApiCulture = 'en' const entries = [ { id: 1, name: 'Item 1', name_plurial: 'Items 1' }, { id: 2, name: 'Item 2', name_plurial: 'Items 2' }, ] it('should not throw when setting entries', async () => { const catalogName: CatalogName = 'book_step' await expect(cache.setEntries(catalogName, culture, entries)).resolves.toBeUndefined() }) it('should handle empty entries array', async () => { const catalogName: CatalogName = 'book_step' await expect(cache.setEntries(catalogName, culture, [])).resolves.toBeUndefined() }) it('should handle different catalog and culture combinations', async () => { await expect(cache.setEntries('book_step', 'en', entries)).resolves.toBeUndefined() await expect(cache.setEntries('property_land', 'fr', entries)).resolves.toBeUndefined() await expect(cache.setEntries('property_type', 'de', entries)).resolves.toBeUndefined() }) }) describe('getEntry', () => { const culture: ApiCulture = 'en' it('should always throw CacheExpiredError regardless of parameters', async () => { const catalogName: CatalogName = 'book_step' await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError) }) it('should throw CacheExpiredError for any ID', async () => { const catalogName: CatalogName = 'book_step' await expect(cache.getEntry(catalogName, culture, 999)).rejects.toThrow(CacheExpiredError) await expect(cache.getEntry(catalogName, culture, 0)).rejects.toThrow(CacheExpiredError) await expect(cache.getEntry(catalogName, culture, -1)).rejects.toThrow(CacheExpiredError) }) it('should throw CacheExpiredError for different catalogs and cultures', async () => { await expect(cache.getEntry('book_step', 'en', 1)).rejects.toThrow(CacheExpiredError) await expect(cache.getEntry('property_land', 'fr', 1)).rejects.toThrow(CacheExpiredError) await expect(cache.getEntry('property_type', 'de', 1)).rejects.toThrow(CacheExpiredError) }) it('should throw CacheExpiredError even after setting entries', async () => { const catalogName: CatalogName = 'book_step' const entries = [{ id: 1, name: 'Item 1', name_plurial: 'Items 1' }] await cache.setEntries(catalogName, culture, entries) await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError) }) }) describe('behavior consistency', () => { it('should behave consistently across multiple calls', async () => { const catalogName: CatalogName = 'book_step' const culture: ApiCulture = 'en' const entries = [{ id: 1, name: 'Item 1', name_plurial: 'Items 1' }] // Multiple setEntries calls should not throw await expect(cache.setEntries(catalogName, culture, entries)).resolves.toBeUndefined() await expect(cache.setEntries(catalogName, culture, entries)).resolves.toBeUndefined() // Multiple getEntry calls should always throw await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError) await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError) }) it('should maintain dummy behavior regardless of cache state', async () => { const catalogName: CatalogName = 'book_step' const culture: ApiCulture = 'en' // Should throw before any operations await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError) // Should still throw after setting entries await cache.setEntries(catalogName, culture, [{ id: 1, name: 'Test', name_plurial: 'Tests' }]) await expect(cache.getEntry(catalogName, culture, 1)).rejects.toThrow(CacheExpiredError) // Should still throw after multiple operations await cache.setEntries(catalogName, culture, []) await expect(cache.getEntry(catalogName, culture, 999)).rejects.toThrow(CacheExpiredError) }) }) })" />
|
|
37
|
-
</PendingDiffInfo>
|
|
38
|
-
</value>
|
|
39
|
-
</entry>
|
|
40
|
-
</map>
|
|
41
|
-
</option>
|
|
42
|
-
</component>
|
|
43
|
-
</project>
|
package/.idea/modules.xml
DELETED
package/.idea/prettier.xml
DELETED
package/.idea/vcs.xml
DELETED
package/eslint.config.mjs
DELETED
package/src/consts/catalogs.ts
DELETED
|
@@ -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]
|
package/src/consts/languages.ts
DELETED
|
@@ -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]
|
package/src/core/api.test.ts
DELETED
|
@@ -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
|
-
})
|