@outlawdesigns/loe-rest-client 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # LOE REST Client (JavaScript)
2
+
3
+ A lightweight JavaScript/Node.js client for interacting with the **Library of Everything (LOE)** REST API using OAuth2 authentication.
4
+ This package provides modular access to LOE models—anime, docs, episodes, movies, songs, and holding-bay items—along with robust token handling and a shared singleton interface.
5
+
6
+ ---
7
+
8
+ ## 🚀 Features
9
+
10
+ * 🔐 **OAuth2 client credentials authentication**
11
+ * 🔄 **Automatic token refresh**
12
+ * 📦 Organized model access:
13
+
14
+ * `anime`
15
+ * `doc`
16
+ * `episode`
17
+ * `movie`
18
+ * `song`
19
+ * `holdingbay` (special pre-ingest items)
20
+ * 🧩 **Singleton mode** for shared instances
21
+ * ⚙️ Clean, consistent `axios`-based API
22
+
23
+ ---
24
+
25
+ ## 📦 Installation
26
+
27
+ ```bash
28
+ npm install @outlawdesigns/loe-rest-client
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 🧠 Basic Usage
34
+
35
+ ### Initialize the API Client
36
+
37
+ ```js
38
+ import loeClient from '@outlawdesigns/loe-rest-client';
39
+
40
+ // Create the singleton instance
41
+ loeClient.init('https://loe-service.outlawdesigns.io', 'openid profile email');
42
+
43
+ // Initialize OAuth provider
44
+ await loeClient.get().auth.init(
45
+ 'https://auth.outlawdesigns.io/oauth2/token',
46
+ 'your-client-id',
47
+ 'your-client-secret'
48
+ );
49
+
50
+ // Perform OAuth2 client credentials flow
51
+ await loeClient.get().auth.clientCredentialFlow(
52
+ 'openid profile email',
53
+ [ 'https://loe-service.outlawdesigns.io' ]
54
+ );
55
+
56
+ // Example call: fetch 3 most recent songs
57
+ const recent = await loeClient.get().songs.getRecent(3);
58
+ console.log(recent);
59
+ ```
60
+
61
+ ### Token Verification Example
62
+
63
+ ```js
64
+ const token = loeClient.get().auth.getAccessToken();
65
+
66
+ const tokenResp = await loeClient.get().auth.verifyAccessToken(
67
+ token,
68
+ ['https://loe-service.outlawdesigns.io']
69
+ );
70
+
71
+ console.log(tokenResp);
72
+ ```
73
+
74
+ ### Accessing the Singleton Anywhere
75
+
76
+ ```js
77
+ import loeClient from '@outlawdesigns/loe-rest-client';
78
+
79
+ const song = await loeClient.get().song.get(42);
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 🔐 Authentication Flow
85
+
86
+ The LOE REST Client uses **`@outlawdesigns/authenticationclient`** to handle:
87
+
88
+ * Token acquisition
89
+ * Token refresh
90
+ * Attaching `Authorization: Bearer <token>` headers
91
+ * Token verification
92
+
93
+ ### Typical Flow
94
+
95
+ ```js
96
+ // 1. init() creates the API + axios wrapper
97
+ loeClient.init(apiUrl, oauthScope);
98
+
99
+ // 2. Provide OAuth endpoint + credentials
100
+ await loeClient.get().auth.init(authUrl, clientId, clientSecret);
101
+
102
+ // 3. Client credentials flow to obtain tokens
103
+ await loeClient.get().auth.clientCredentialFlow(oauthScope, [apiUrl]);
104
+
105
+ // 4. Make API calls
106
+ const movies = await loeClient.get().movie.getAll();
107
+ ```
108
+
109
+ ---
110
+
111
+ ## 🧩 Available Model Modules
112
+
113
+ ### 🎵 `api.song`
114
+
115
+ Supports LOE’s rich music feature set.
116
+
117
+ | Method | Description |
118
+ | --------------------------------- | -------------------------------- |
119
+ | `getAll()` | Get all songs |
120
+ | `get(id)` | Fetch a song |
121
+ | `create(songObj)` | Create a song |
122
+ | `search(field, query)` | Search songs |
123
+ | `browse(field)` | Browse unique values for a field |
124
+ | `getRecent(limit)` | Fetch `n` most recent songs |
125
+ | `getMyPlaylists()` | Retrieve user playlists |
126
+ | `getPlaylist(id)` | Get a specific playlist |
127
+ | `savePlaylist(obj)` | Save playlist |
128
+ | `rate(id, rating)` | Rate a song |
129
+ | `getRating(id)` | Fetch a rating |
130
+ | `count()` | Count total songs |
131
+ | `group(field)` | Group by field |
132
+ | `getRandomPlaylist(genre, limit)` | Randomized playlist |
133
+
134
+ ---
135
+
136
+ ### 🎬 `api.movie`
137
+
138
+ Standard CRUD + metadata operations.
139
+
140
+ ### 📺 `api.episode`
141
+
142
+ Episode-level metadata for TV/anime content.
143
+
144
+ ### 🎭 `api.anime`
145
+
146
+ Anime metadata (similar to episode model).
147
+
148
+ ### 📄 `api.doc`
149
+
150
+ Document/Ebook metadata.
151
+
152
+ ### 📥 `api.holdingbay`
153
+
154
+ Read-only staging area for media not yet classified:
155
+
156
+ | Method | Description |
157
+ | ------------- | ----------------------------------- |
158
+ | `getMovies()` | Items possibly classified as movies |
159
+ | `getSongs()` | Music-type items |
160
+ | `getTv()` | TV/episode candidates |
161
+ | `getComics()` | Comic content |
162
+
163
+ ---
164
+
165
+ ## 🧱 Project Structure
166
+
167
+ ```
168
+ src/
169
+ ├── core.js
170
+ ├── singleton.js
171
+ ├── models/
172
+ │ ├── anime.js
173
+ │ ├── doc.js
174
+ │ ├── episode.js
175
+ │ ├── movie.js
176
+ │ ├── song.js
177
+ │ └── holdingbay.js
178
+ └── formData.js
179
+ ```
180
+
181
+ ---
182
+
183
+ ## ⚙️ Creating a Direct Client (Non-Singleton)
184
+
185
+ ```js
186
+ import { createApiClient } from '@outlawdesigns/loe-rest-client/core.js';
187
+
188
+ const api = createApiClient(apiUrl, oauthScope);
189
+ await api.auth.init(authUrl, clientId, clientSecret);
190
+ await api.auth.clientCredentialFlow(oauthScope, [apiUrl]);
191
+ ```
192
+
193
+ ---
194
+
195
+ ## 👤 Author
196
+
197
+ Maintained by **Outlaw Designs**
198
+ [https://github.com/outlawdesigns-io](https://github.com/outlawdesigns-io)
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import apiClientSingleton from './src/singleton.js';
2
+
3
+ export default apiClientSingleton;
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@outlawdesigns/loe-rest-client",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "scripts": {
6
+ "test": "echo \"Error: no test specified\" && exit 1"
7
+ },
8
+ "type": "module",
9
+ "author": "",
10
+ "license": "ISC",
11
+ "description": "",
12
+ "dependencies": {
13
+ "@outlawdesigns/authenticationclient": "^2.1.1",
14
+ "axios": "^1.13.2"
15
+ }
16
+ }
package/src/core.js ADDED
@@ -0,0 +1,54 @@
1
+ import axios from 'axios';
2
+ import authClient from '@outlawdesigns/authenticationclient';
3
+
4
+ import createAnime from './models/anime.js';
5
+ import createDocs from './models/doc.js';
6
+ import createEpisodes from './models/episode.js';
7
+ import createHoldingbay from './models/holdingBay.js';
8
+ import createMovies from './models/movie.js';
9
+ import createSongs from './models/song.js';
10
+
11
+ export function createApiClient(baseURL, requestedScope){
12
+ const oauthScope = requestedScope; //the scope(s) for this app
13
+ const oauthResource = baseURL;
14
+ const oauthRefreshBuffer = 300;
15
+ const axiosInstance = axios.create({baseURL:baseURL});
16
+ authClient.onTokenUpdate((token)=>{
17
+ axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${token}`;
18
+ });
19
+ axiosInstance.interceptors.request.use(async (config)=>{
20
+ const token = authClient.getAccessToken();
21
+ if(!token) throw new Error(`Authenticate before making API calls.`);
22
+ const refreshToken = authClient.getRefreshToken();
23
+ let user;
24
+ if(refreshToken){
25
+ try{
26
+ user = await authClient.verifyAccessToken(token,[oauthResource]);
27
+ }catch(err){
28
+ console.log(err);
29
+ return;
30
+ }
31
+ const now = Math.floor(Date.now() / 1000);
32
+ const timeDiffSeconds = user.exp - now;
33
+ if(timeDiffSeconds <= oauthRefreshBuffer){
34
+ try{
35
+ await authClient.refreshToken(oauthScope,[oauthResource]);
36
+ }catch(err){
37
+ console.log(err);
38
+ return;
39
+ }
40
+ }
41
+ }
42
+ config.headers['Authorization'] = `Bearer ${authClient.getAccessToken()}`;
43
+ return config;
44
+ });
45
+ return {
46
+ auth:authClient,
47
+ movies:createMovies(axiosInstance),
48
+ songs:createSongs(axiosInstance),
49
+ holdingBay:createHoldingbay(axiosInstance),
50
+ episodes:createEpisodes(axiosInstance),
51
+ docs:createDocs(axiosInstance),
52
+ anime:createAnime(axiosInstance)
53
+ }
54
+ }
@@ -0,0 +1,65 @@
1
+ const resource = '/anime';
2
+
3
+ export default function createAnime(axios){
4
+ return {
5
+ async getAll(){
6
+ const res = await axios.get(`${resource}`);
7
+ return res.data;
8
+ },
9
+ async get(id){
10
+ if(!id){
11
+ throw new Error('Id argument reguired.');
12
+ }
13
+ const res = await axios.get(`${resource}/${id}`);
14
+ return res.data;
15
+ },
16
+ async create(animeObj){
17
+ const res = await axios.post(`${resource}`,animeObj);
18
+ return res.data;
19
+ },
20
+ async search(field, query){
21
+ const res = await axios.get(`${resource}/search/${field}/${query}`);
22
+ return res.data;
23
+ },
24
+ async browse(field){
25
+ const res = await axios.get(`${resource}/browse/${field}`);
26
+ return res.data;
27
+ },
28
+ async getRecent(limit){
29
+ const res = await axios.get(`${resource}/recent/${limit}`);
30
+ return res.data;
31
+ },
32
+ async getMyPlaylists(){
33
+ const res = await axios.get(`${resource}/list/`);
34
+ return res.data;
35
+ },
36
+ async getPlaylist(id){
37
+ const res = await axios.get(`${resource}/list/${id}`);
38
+ return res.data;
39
+ },
40
+ async savePlaylist(playlistObj){
41
+ const res = await axios.post(`${resource}/list`,playlistObj);
42
+ return res.data;
43
+ },
44
+ async rate(animeId, rating){
45
+ const res = await axios.post(`${resource}/rate/${animeId}`,{rating:rating});
46
+ return res.data;
47
+ },
48
+ async getRating(id){
49
+ const res = await axios.get(`${resource}/rate/${id}`);
50
+ return res.data;
51
+ },
52
+ async count(){
53
+ const res = await axios.get(`${resource}/count/`);
54
+ return res.data;
55
+ },
56
+ async group(field){
57
+ const res = await axios.get(`${resource}/group/${field}`);
58
+ return res.data;
59
+ },
60
+ async getRandomPlaylist(genre, limit){
61
+ const res = await axios.get(`${resource}/random/${genre}/${limit}`);
62
+ return res.data;
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,65 @@
1
+ const resource = '/doc';
2
+
3
+ export default function createDocs(axios){
4
+ return {
5
+ async getAll(){
6
+ const res = await axios.get(`${resource}`);
7
+ return res.data;
8
+ },
9
+ async get(id){
10
+ if(!id){
11
+ throw new Error('Id argument reguired.');
12
+ }
13
+ const res = await axios.get(`${resource}/${id}`);
14
+ return res.data;
15
+ },
16
+ async create(docObj){
17
+ const res = await axios.post(`${resource}`,docObj);
18
+ return res.data;
19
+ },
20
+ async search(field, query){
21
+ const res = await axios.get(`${resource}/search/${field}/${query}`);
22
+ return res.data;
23
+ },
24
+ async browse(field){
25
+ const res = await axios.get(`${resource}/browse/${field}`);
26
+ return res.data;
27
+ },
28
+ async getRecent(limit){
29
+ const res = await axios.get(`${resource}/recent/${limit}`);
30
+ return res.data;
31
+ },
32
+ async getMyPlaylists(){
33
+ const res = await axios.get(`${resource}/list/`);
34
+ return res.data;
35
+ },
36
+ async getPlaylist(id){
37
+ const res = await axios.get(`${resource}/list/${id}`);
38
+ return res.data;
39
+ },
40
+ async savePlaylist(playlistObj){
41
+ const res = await axios.post(`${resource}/list`,playlistObj);
42
+ return res.data;
43
+ },
44
+ async rate(docId, rating){
45
+ const res = await axios.post(`${resource}/rate/${docId}`,{rating:rating});
46
+ return res.data;
47
+ },
48
+ async getRating(id){
49
+ const res = await axios.get(`${resource}/rate/${id}`);
50
+ return res.data;
51
+ },
52
+ async count(){
53
+ const res = await axios.get(`${resource}/count/`);
54
+ return res.data;
55
+ },
56
+ async group(field){
57
+ const res = await axios.get(`${resource}/group/${field}`);
58
+ return res.data;
59
+ },
60
+ async getRandomPlaylist(genre, limit){
61
+ const res = await axios.get(`${resource}/random/${genre}/${limit}`);
62
+ return res.data;
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,65 @@
1
+ const resource = '/episode';
2
+
3
+ export default function createEpisodes(axios){
4
+ return {
5
+ async getAll(){
6
+ const res = await axios.get(`${resource}`);
7
+ return res.data;
8
+ },
9
+ async get(id){
10
+ if(!id){
11
+ throw new Error('Id argument reguired.');
12
+ }
13
+ const res = await axios.get(`${resource}/${id}`);
14
+ return res.data;
15
+ },
16
+ async create(episodeObj){
17
+ const res = await axios.post(`${resource}`,episodeObj);
18
+ return res.data;
19
+ },
20
+ async search(field, query){
21
+ const res = await axios.get(`${resource}/search/${field}/${query}`);
22
+ return res.data;
23
+ },
24
+ async browse(field){
25
+ const res = await axios.get(`${resource}/browse/${field}`);
26
+ return res.data;
27
+ },
28
+ async getRecent(limit){
29
+ const res = await axios.get(`${resource}/recent/${limit}`);
30
+ return res.data;
31
+ },
32
+ async getMyPlaylists(){
33
+ const res = await axios.get(`${resource}/list/`);
34
+ return res.data;
35
+ },
36
+ async getPlaylist(id){
37
+ const res = await axios.get(`${resource}/list/${id}`);
38
+ return res.data;
39
+ },
40
+ async savePlaylist(playlistObj){
41
+ const res = await axios.post(`${resource}/list`,playlistObj);
42
+ return res.data;
43
+ },
44
+ async rate(episodeId, rating){
45
+ const res = await axios.post(`${resource}/rate/${episodeId}`,{rating:rating});
46
+ return res.data;
47
+ },
48
+ async getRating(id){
49
+ const res = await axios.get(`${resource}/rate/${id}`);
50
+ return res.data;
51
+ },
52
+ async count(){
53
+ const res = await axios.get(`${resource}/count/`);
54
+ return res.data;
55
+ },
56
+ async group(field){
57
+ const res = await axios.get(`${resource}/group/${field}`);
58
+ return res.data;
59
+ },
60
+ async getRandomPlaylist(genre, limit){
61
+ const res = await axios.get(`${resource}/random/${genre}/${limit}`);
62
+ return res.data;
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,22 @@
1
+ const resource = '/holdingbay';
2
+
3
+ export default function createHoldingbay(axios){
4
+ return {
5
+ async getMovies(){
6
+ const res = await axios.get(`${resource}/movies`);
7
+ return res.data;
8
+ },
9
+ async getSongs(){
10
+ const res = await axios.get(`${resource}/music`);
11
+ return res.data;
12
+ },
13
+ async getTv(){
14
+ const res = await axios.get(`${resource}/tv`);
15
+ return res.data;
16
+ },
17
+ async getComics(){
18
+ const res = await axios.get(`${resource}/comic`);
19
+ return res.data;
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,65 @@
1
+ const resource = '/movie';
2
+
3
+ export default function createMovies(axios){
4
+ return {
5
+ async getAll(){
6
+ const res = await axios.get(`${resource}`);
7
+ return res.data;
8
+ },
9
+ async get(id){
10
+ if(!id){
11
+ throw new Error('Id argument reguired.');
12
+ }
13
+ const res = await axios.get(`${resource}/${id}`);
14
+ return res.data;
15
+ },
16
+ async create(movieObj){
17
+ const res = await axios.post(`${resource}`,movieObj);
18
+ return res.data;
19
+ },
20
+ async search(field, query){
21
+ const res = await axios.get(`${resource}/search/${field}/${query}`);
22
+ return res.data;
23
+ },
24
+ async browse(field){
25
+ const res = await axios.get(`${resource}/browse/${field}`);
26
+ return res.data;
27
+ },
28
+ async getRecent(limit){
29
+ const res = await axios.get(`${resource}/recent/${limit}`);
30
+ return res.data;
31
+ },
32
+ async getMyPlaylists(){
33
+ const res = await axios.get(`${resource}/list/`);
34
+ return res.data;
35
+ },
36
+ async getPlaylist(id){
37
+ const res = await axios.get(`${resource}/list/${id}`);
38
+ return res.data;
39
+ },
40
+ async savePlaylist(playlistObj){
41
+ const res = await axios.post(`${resource}/list`,playlistObj);
42
+ return res.data;
43
+ },
44
+ async rate(movieId, rating){
45
+ const res = await axios.post(`${resource}/rate/${movieId}`,{rating:rating});
46
+ return res.data;
47
+ },
48
+ async getRating(id){
49
+ const res = await axios.get(`${resource}/rate/${id}`);
50
+ return res.data;
51
+ },
52
+ async count(){
53
+ const res = await axios.get(`${resource}/count/`);
54
+ return res.data;
55
+ },
56
+ async group(field){
57
+ const res = await axios.get(`${resource}/group/${field}`);
58
+ return res.data;
59
+ },
60
+ async getRandomPlaylist(genre, limit){
61
+ const res = await axios.get(`${resource}/random/${genre}/${limit}`);
62
+ return res.data;
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,65 @@
1
+ const resource = '/song';
2
+
3
+ export default function createSongs(axios){
4
+ return {
5
+ async getAll(){
6
+ const res = await axios.get(`${resource}`);
7
+ return res.data;
8
+ },
9
+ async get(id){
10
+ if(!id){
11
+ throw new Error('Id argument reguired.');
12
+ }
13
+ const res = await axios.get(`${resource}/${id}`);
14
+ return res.data;
15
+ },
16
+ async create(songObj){
17
+ const res = await axios.post(`${resource}`,songObj);
18
+ return res.data;
19
+ },
20
+ async search(field, query){
21
+ const res = await axios.get(`${resource}/search/${field}/${query}`);
22
+ return res.data;
23
+ },
24
+ async browse(field){
25
+ const res = await axios.get(`${resource}/browse/${field}`);
26
+ return res.data;
27
+ },
28
+ async getRecent(limit){
29
+ const res = await axios.get(`${resource}/recent/${limit}`);
30
+ return res.data;
31
+ },
32
+ async getMyPlaylists(){
33
+ const res = await axios.get(`${resource}/list/`);
34
+ return res.data;
35
+ },
36
+ async getPlaylist(id){
37
+ const res = await axios.get(`${resource}/list/${id}`);
38
+ return res.data;
39
+ },
40
+ async savePlaylist(playlistObj){
41
+ const res = await axios.post(`${resource}/list`,playlistObj);
42
+ return res.data;
43
+ },
44
+ async rate(songId, rating){
45
+ const res = await axios.post(`${resource}/rate/${songId}`,{rating:rating});
46
+ return res.data;
47
+ },
48
+ async getRating(id){
49
+ const res = await axios.get(`${resource}/rate/${id}`);
50
+ return res.data;
51
+ },
52
+ async count(){
53
+ const res = await axios.get(`${resource}/count/`);
54
+ return res.data;
55
+ },
56
+ async group(field){
57
+ const res = await axios.get(`${resource}/group/${field}`);
58
+ return res.data;
59
+ },
60
+ async getRandomPlaylist(genre, limit){
61
+ const res = await axios.get(`${resource}/random/${genre}/${limit}`);
62
+ return res.data;
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,26 @@
1
+ import { createApiClient } from './core.js';
2
+
3
+ let instance = null;
4
+
5
+ function init(baseURL){
6
+ if(instance){
7
+ if(instance.axios.defaults.baseURL !== baseURL){
8
+ throw new Error(`API client already initialized with ${instance.axios.defaults.baseURL}`);
9
+ }
10
+ return instance;
11
+ }
12
+ instance = createApiClient(baseURL);
13
+ return instance;
14
+ }
15
+
16
+ function getInstance(){
17
+ if(!instance){
18
+ throw new Error('API client not initialized. Call init(baseURL) first.');
19
+ }
20
+ return instance;
21
+ }
22
+
23
+ export default{
24
+ init,
25
+ get: getInstance
26
+ };