Compare commits
11 Commits
90b7b1cf01
..
v1.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
adf1836fce
|
|||
|
e461932aa9
|
|||
|
d36bef185c
|
|||
|
b84010a332
|
|||
|
05516e7642
|
|||
|
22c6b02e4b
|
|||
|
e859bc3ee7
|
|||
|
b3fd092acf
|
|||
|
cd349944cf
|
|||
|
6b434adde4
|
|||
|
a966df95f0
|
@@ -0,0 +1,17 @@
|
|||||||
|
name: Test
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
- run: pnpm install
|
||||||
|
- run: pnpm test
|
||||||
@@ -26,8 +26,10 @@ It is written in TypeScript and compiled to a JavaScript module suitable for use
|
|||||||
- `dist/`: specific build artifacts. Do not edit files here directly.
|
- `dist/`: specific build artifacts. Do not edit files here directly.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
- Currently, no automated test suite is configured.
|
- **Framework:** `vitest`
|
||||||
- `pnpm test` will exit with an error.
|
- **Run tests:** `pnpm test`
|
||||||
|
- **Watch mode:** `pnpm run test:watch`
|
||||||
|
- **Location:** Tests are located in the `test/` directory.
|
||||||
|
|
||||||
## Contribution Guidelines
|
## Contribution Guidelines
|
||||||
- When adding new functionality, ensure proper types are exported in `src/types.d.ts` or within the module files.
|
- When adding new functionality, ensure proper types are exported in `src/types.d.ts` or within the module files.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# @remotestorage/module-places
|
# @remotestorage/module-places
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/@remotestorage/module-places)
|
[](https://www.npmjs.com/package/@remotestorage/module-places) [](https://gitea.kosmos.org/raucao/remotestorage-module-places/actions)
|
||||||
|
|
||||||
This module allows you to manage saved places (Points of Interest) using the [remoteStorage](https://remotestorage.io/) protocol.
|
This module allows you to manage saved places (Points of Interest) using the [remoteStorage](https://remotestorage.io/) protocol.
|
||||||
|
|
||||||
@@ -11,7 +11,17 @@ For a demo application, as well as source code using this module, check out [Mar
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# npm
|
||||||
|
npm install @remotestorage/module-places
|
||||||
|
|
||||||
|
# pnpm
|
||||||
pnpm add @remotestorage/module-places
|
pnpm add @remotestorage/module-places
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn add @remotestorage/module-places
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun add @remotestorage/module-places
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -41,12 +51,60 @@ console.log(allPlaces);
|
|||||||
// List places for specific geohash prefixes (e.g. for a map view)
|
// List places for specific geohash prefixes (e.g. for a map view)
|
||||||
const areaPlaces = await places.getPlaces(['u33d', 'u33e']);
|
const areaPlaces = await places.getPlaces(['u33d', 'u33e']);
|
||||||
console.log(areaPlaces);
|
console.log(areaPlaces);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lists
|
||||||
|
|
||||||
|
### Default lists
|
||||||
|
|
||||||
|
There are currently two default lists, which you can initiate like this:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await places.lists.initDefaults();
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create the lists if they don't exist yet (meaning the user hasn't yet
|
||||||
|
used an app that integrates this module).
|
||||||
|
|
||||||
|
The default lists are:
|
||||||
|
|
||||||
|
| Path | Default Name | Default Color |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `_lists/to-go` | Want to go | #2e9e4f (green) |
|
||||||
|
| `_lists/to-do` | To do | #2a7fff (blue) |
|
||||||
|
|
||||||
|
### Custom lists
|
||||||
|
|
||||||
|
```javascript
|
||||||
// Create a list
|
// Create a list
|
||||||
await places.lists.create('favorites', 'My Favorites');
|
await places.lists.create('hiking', 'Hiking', '#74d3ba');
|
||||||
|
|
||||||
|
// Delete a list
|
||||||
|
await places.lists.delete('hiking');
|
||||||
|
```
|
||||||
|
|
||||||
|
### List membership
|
||||||
|
|
||||||
|
```javascript
|
||||||
// Add a place to a list (requires list ID, place ID, and place geohash)
|
// Add a place to a list (requires list ID, place ID, and place geohash)
|
||||||
await places.lists.addPlace('favorites', 'place-id-123', 'u33dc0');
|
await places.lists.addPlace('to-go', 'place-id-123', 'u33dc0');
|
||||||
|
|
||||||
|
// Remove from list
|
||||||
|
await places.lists.removePlace('to-go', 'place-id-123');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reading lists
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get all lists
|
||||||
|
await places.lists.getAll();
|
||||||
|
|
||||||
|
// Get specific list
|
||||||
|
await places.lists.get('to-do');
|
||||||
|
|
||||||
|
// Get all places from a list
|
||||||
|
await places.lists.getPlaces('to-do');
|
||||||
|
```
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|||||||
Vendored
+19
@@ -96,6 +96,19 @@ declare const listSchema: {
|
|||||||
};
|
};
|
||||||
readonly required: readonly ["id", "title", "placeRefs", "createdAt"];
|
readonly required: readonly ["id", "title", "placeRefs", "createdAt"];
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Represents a List object.
|
||||||
|
*
|
||||||
|
* Core properties enforced by schema:
|
||||||
|
* - `id`: Unique identifier (slug)
|
||||||
|
* - `title`: Human readable title
|
||||||
|
* - `placeRefs`: Array of place references containing `id` and `geohash`
|
||||||
|
* - `createdAt`: ISO date string
|
||||||
|
*
|
||||||
|
* Optional properties:
|
||||||
|
* - `color`: Hex color code
|
||||||
|
* - `updatedAt`: ISO date string
|
||||||
|
*/
|
||||||
export type List = FromSchema<typeof listSchema> & {
|
export type List = FromSchema<typeof listSchema> & {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
@@ -179,6 +192,12 @@ export interface PlacesClient {
|
|||||||
* @param id - The slug ID of the list.
|
* @param id - The slug ID of the list.
|
||||||
*/
|
*/
|
||||||
get(id: string): Promise<List | null>;
|
get(id: string): Promise<List | null>;
|
||||||
|
/**
|
||||||
|
* Get all places from a list.
|
||||||
|
* @param listId - The slug ID of the list.
|
||||||
|
* @returns Array of Place objects.
|
||||||
|
*/
|
||||||
|
getPlaces(listId: string): Promise<Place[]>;
|
||||||
/**
|
/**
|
||||||
* Create or update a list.
|
* Create or update a list.
|
||||||
* @param id - The slug ID (e.g., "to-go").
|
* @param id - The slug ID (e.g., "to-go").
|
||||||
|
|||||||
Vendored
+18
@@ -103,6 +103,24 @@ const Places = function (privateClient /*, publicClient: BaseClient */) {
|
|||||||
const path = `_lists/${id}`;
|
const path = `_lists/${id}`;
|
||||||
return privateClient.getObject(path);
|
return privateClient.getObject(path);
|
||||||
},
|
},
|
||||||
|
async getPlaces(listId) {
|
||||||
|
const list = await this.get(listId);
|
||||||
|
if (!list) {
|
||||||
|
throw new Error(`List not found: ${listId}`);
|
||||||
|
}
|
||||||
|
if (!list.placeRefs || !Array.isArray(list.placeRefs)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const promises = list.placeRefs.map(async (ref) => {
|
||||||
|
if (!ref.id || !ref.geohash)
|
||||||
|
return null;
|
||||||
|
const path = getPath(ref.geohash, ref.id);
|
||||||
|
const place = await privateClient.getObject(path);
|
||||||
|
return place;
|
||||||
|
});
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
return results.filter((p) => !!p);
|
||||||
|
},
|
||||||
async create(id, title, color) {
|
async create(id, title, color) {
|
||||||
const path = `_lists/${id}`;
|
const path = `_lists/${id}`;
|
||||||
let list = (await privateClient.getObject(path));
|
let list = (await privateClient.getObject(path));
|
||||||
|
|||||||
@@ -120,6 +120,26 @@ Get all lists.
|
|||||||
|
|
||||||
Array of List objects.
|
Array of List objects.
|
||||||
|
|
||||||
|
#### getPlaces()
|
||||||
|
|
||||||
|
> **getPlaces**(`listId`): `Promise`\<[`Place`](../type-aliases/Place.md)[]\>
|
||||||
|
|
||||||
|
Get all places from a list.
|
||||||
|
|
||||||
|
##### Parameters
|
||||||
|
|
||||||
|
###### listId
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
The slug ID of the list.
|
||||||
|
|
||||||
|
##### Returns
|
||||||
|
|
||||||
|
`Promise`\<[`Place`](../type-aliases/Place.md)[]\>
|
||||||
|
|
||||||
|
Array of Place objects.
|
||||||
|
|
||||||
#### removePlace()
|
#### removePlace()
|
||||||
|
|
||||||
> **removePlace**(`listId`, `placeId`): `Promise`\<[`List`](../type-aliases/List.md)\>
|
> **removePlace**(`listId`, `placeId`): `Promise`\<[`List`](../type-aliases/List.md)\>
|
||||||
|
|||||||
@@ -7,3 +7,15 @@
|
|||||||
# Type Alias: List
|
# Type Alias: List
|
||||||
|
|
||||||
> **List** = `FromSchema`\<*typeof* `listSchema`\> & `object`
|
> **List** = `FromSchema`\<*typeof* `listSchema`\> & `object`
|
||||||
|
|
||||||
|
Represents a List object.
|
||||||
|
|
||||||
|
Core properties enforced by schema:
|
||||||
|
- `id`: Unique identifier (slug)
|
||||||
|
- `title`: Human readable title
|
||||||
|
- `placeRefs`: Array of place references containing `id` and `geohash`
|
||||||
|
- `createdAt`: ISO date string
|
||||||
|
|
||||||
|
Optional properties:
|
||||||
|
- `color`: Hex color code
|
||||||
|
- `updatedAt`: ISO date string
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@remotestorage/module-places",
|
"name": "@remotestorage/module-places",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@remotestorage/module-places",
|
"name": "@remotestorage/module-places",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"latlon-geohash": "^2.0.0",
|
"latlon-geohash": "^2.0.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@remotestorage/module-places",
|
"name": "@remotestorage/module-places",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"description": "Manage favorite/saved places",
|
"description": "Manage favorite/saved places",
|
||||||
"homepage": "https://gitea.kosmos.org/raucao/remotestorage-module-places#remotestoragemodule-places",
|
"homepage": "https://gitea.kosmos.org/raucao/remotestorage-module-places#remotestoragemodule-places",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -55,6 +55,19 @@ const listSchema = {
|
|||||||
required: ['id', 'title', 'placeRefs', 'createdAt'],
|
required: ['id', 'title', 'placeRefs', 'createdAt'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a List object.
|
||||||
|
*
|
||||||
|
* Core properties enforced by schema:
|
||||||
|
* - `id`: Unique identifier (slug)
|
||||||
|
* - `title`: Human readable title
|
||||||
|
* - `placeRefs`: Array of place references containing `id` and `geohash`
|
||||||
|
* - `createdAt`: ISO date string
|
||||||
|
*
|
||||||
|
* Optional properties:
|
||||||
|
* - `color`: Hex color code
|
||||||
|
* - `updatedAt`: ISO date string
|
||||||
|
*/
|
||||||
export type List = FromSchema<typeof listSchema> & { [key: string]: any };
|
export type List = FromSchema<typeof listSchema> & { [key: string]: any };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,6 +154,13 @@ export interface PlacesClient {
|
|||||||
*/
|
*/
|
||||||
get(id: string): Promise<List | null>;
|
get(id: string): Promise<List | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all places from a list.
|
||||||
|
* @param listId - The slug ID of the list.
|
||||||
|
* @returns Array of Place objects.
|
||||||
|
*/
|
||||||
|
getPlaces(listId: string): Promise<Place[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create or update a list.
|
* Create or update a list.
|
||||||
* @param id - The slug ID (e.g., "to-go").
|
* @param id - The slug ID (e.g., "to-go").
|
||||||
@@ -236,6 +256,27 @@ const Places = function (
|
|||||||
return privateClient.getObject(path) as Promise<List | null>;
|
return privateClient.getObject(path) as Promise<List | null>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getPlaces(listId: string): Promise<Place[]> {
|
||||||
|
const list = await this.get(listId);
|
||||||
|
if (!list) {
|
||||||
|
throw new Error(`List not found: ${listId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!list.placeRefs || !Array.isArray(list.placeRefs)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises = list.placeRefs.map(async (ref: any) => {
|
||||||
|
if (!ref.id || !ref.geohash) return null;
|
||||||
|
const path = getPath(ref.geohash, ref.id);
|
||||||
|
const place = await privateClient.getObject(path);
|
||||||
|
return place as Place | null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
return results.filter((p): p is Place => !!p);
|
||||||
|
},
|
||||||
|
|
||||||
async create(id: string, title: string, color?: string): Promise<List> {
|
async create(id: string, title: string, color?: string): Promise<List> {
|
||||||
const path = `_lists/${id}`;
|
const path = `_lists/${id}`;
|
||||||
let list = (await privateClient.getObject(path)) as List;
|
let list = (await privateClient.getObject(path)) as List;
|
||||||
|
|||||||
@@ -203,6 +203,64 @@ describe('Places Module', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getPlaces', () => {
|
||||||
|
it('returns all places from a list', async () => {
|
||||||
|
const mockList = {
|
||||||
|
id: 'hiking',
|
||||||
|
title: 'Hiking',
|
||||||
|
placeRefs: [
|
||||||
|
{ id: 'place-1', geohash: 'u33dc0' },
|
||||||
|
{ id: 'place-2', geohash: 'w1q789' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPlace1 = { id: 'place-1', geohash: 'u33dc0', title: 'Hiking Trail' };
|
||||||
|
const mockPlace2 = { id: 'place-2', geohash: 'w1q789', title: 'Mountain Peak' };
|
||||||
|
|
||||||
|
mockClient.getObject.mockImplementation(async (path: string) => {
|
||||||
|
if (path === '_lists/hiking') return mockList;
|
||||||
|
if (path === 'u3/3d/place-1') return mockPlace1;
|
||||||
|
if (path === 'w1/q7/place-2') return mockPlace2;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await lists.getPlaces('hiking');
|
||||||
|
expect(mockClient.getObject).toHaveBeenCalledWith('_lists/hiking');
|
||||||
|
expect(mockClient.getObject).toHaveBeenCalledWith('u3/3d/place-1');
|
||||||
|
expect(mockClient.getObject).toHaveBeenCalledWith('w1/q7/place-2');
|
||||||
|
expect(result).toEqual([mockPlace1, mockPlace2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if the list does not exist', async () => {
|
||||||
|
mockClient.getObject.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(lists.getPlaces('non-existent')).rejects.toThrow('List not found: non-existent');
|
||||||
|
expect(mockClient.getObject).toHaveBeenCalledWith('_lists/non-existent');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters out any places that failed to load or are missing', async () => {
|
||||||
|
const mockList = {
|
||||||
|
id: 'hiking',
|
||||||
|
title: 'Hiking',
|
||||||
|
placeRefs: [
|
||||||
|
{ id: 'place-1', geohash: 'u33dc0' },
|
||||||
|
{ id: 'place-2', geohash: 'w1q789' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPlace1 = { id: 'place-1', geohash: 'u33dc0', title: 'Place 1' };
|
||||||
|
|
||||||
|
mockClient.getObject.mockImplementation(async (path: string) => {
|
||||||
|
if (path === '_lists/hiking') return mockList;
|
||||||
|
if (path === 'u3/3d/place-1') return mockPlace1;
|
||||||
|
return null; // place-2 is missing
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await lists.getPlaces('hiking');
|
||||||
|
expect(result).toEqual([mockPlace1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
it('stores a new list when none exists', async () => {
|
it('stores a new list when none exists', async () => {
|
||||||
const now = '2023-01-01T00:00:00.000Z';
|
const now = '2023-01-01T00:00:00.000Z';
|
||||||
|
|||||||
Reference in New Issue
Block a user