@flowerforce/flowerbase-client 0.1.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/CHANGELOG.md +0 -0
- package/LICENSE +3 -0
- package/README.md +209 -0
- package/dist/app.d.ts +85 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +461 -0
- package/dist/bson.d.ts +8 -0
- package/dist/bson.d.ts.map +1 -0
- package/dist/bson.js +10 -0
- package/dist/credentials.d.ts +8 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +30 -0
- package/dist/functions.d.ts +6 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +47 -0
- package/dist/http.d.ts +35 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +170 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/mongo.d.ts +4 -0
- package/dist/mongo.d.ts.map +1 -0
- package/dist/mongo.js +106 -0
- package/dist/session.d.ts +18 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +105 -0
- package/dist/session.native.d.ts +14 -0
- package/dist/session.native.d.ts.map +1 -0
- package/dist/session.native.js +76 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/user.d.ts +37 -0
- package/dist/user.d.ts.map +1 -0
- package/dist/user.js +125 -0
- package/dist/watch.d.ts +3 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +139 -0
- package/jest.config.ts +13 -0
- package/package.json +41 -0
- package/project.json +11 -0
- package/rollup.config.js +17 -0
- package/src/__tests__/auth.test.ts +213 -0
- package/src/__tests__/compat.test.ts +22 -0
- package/src/__tests__/functions.test.ts +312 -0
- package/src/__tests__/mongo.test.ts +83 -0
- package/src/__tests__/session.test.ts +597 -0
- package/src/__tests__/watch.test.ts +336 -0
- package/src/app.ts +562 -0
- package/src/bson.ts +6 -0
- package/src/credentials.ts +31 -0
- package/src/functions.ts +56 -0
- package/src/http.ts +221 -0
- package/src/index.ts +15 -0
- package/src/mongo.ts +112 -0
- package/src/session.native.ts +89 -0
- package/src/session.ts +114 -0
- package/src/types.ts +114 -0
- package/src/user.ts +150 -0
- package/src/watch.ts +156 -0
- package/tsconfig.json +34 -0
- package/tsconfig.spec.json +13 -0
package/CHANGELOG.md
ADDED
|
File without changes
|
package/LICENSE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# @flowerforce/flowerbase-client
|
|
2
|
+
|
|
3
|
+
Client TypeScript leggero per usare Flowerbase con API in stile Realm:
|
|
4
|
+
|
|
5
|
+
- autenticazione (`local-userpass`, `anon-user`, `custom-function`)
|
|
6
|
+
- chiamate funzioni (`user.functions.<name>(...)`)
|
|
7
|
+
- accesso a MongoDB Atlas service (`user.mongoClient("mongodb-atlas")`)
|
|
8
|
+
- change stream via `watch()` con async iterator
|
|
9
|
+
- supporto BSON/EJSON (`ObjectId`, `Date`, ecc.)
|
|
10
|
+
|
|
11
|
+
## Installazione
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i @flowerforce/flowerbase-client
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { App, Credentials } from '@flowerforce/flowerbase-client'
|
|
21
|
+
|
|
22
|
+
const app = new App({
|
|
23
|
+
id: 'my-app-id',
|
|
24
|
+
baseUrl: 'http://localhost:8000',
|
|
25
|
+
timeout: 10000
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
await app.logIn(Credentials.emailPassword('user@example.com', 'secret'))
|
|
29
|
+
|
|
30
|
+
const user = app.currentUser
|
|
31
|
+
if (!user) throw new Error('User not logged in')
|
|
32
|
+
|
|
33
|
+
const result = await user.functions.myFunction('hello')
|
|
34
|
+
console.log(result)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configurazione `App`
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
new App({
|
|
41
|
+
id: string, // app id Flowerbase
|
|
42
|
+
baseUrl: string, // URL base backend (es: http://localhost:8000)
|
|
43
|
+
timeout?: number // default 10000
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Autenticazione
|
|
48
|
+
|
|
49
|
+
### Local user/pass
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
await app.logIn(Credentials.emailPassword(email, password))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Anonymous
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
await app.logIn(Credentials.anonymous())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Custom function auth
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
await app.logIn(
|
|
65
|
+
Credentials.function({
|
|
66
|
+
username: 'demo',
|
|
67
|
+
pin: '1234'
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Utility `emailPasswordAuth`
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
await app.emailPasswordAuth.registerUser({ email, password })
|
|
76
|
+
await app.emailPasswordAuth.sendResetPasswordEmail(email)
|
|
77
|
+
await app.emailPasswordAuth.callResetPasswordFunction(email, newPassword, extraArg1, extraArg2)
|
|
78
|
+
await app.emailPasswordAuth.resetPassword({ token, tokenId, password })
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Current user
|
|
82
|
+
|
|
83
|
+
Dopo il login:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const user = app.currentUser
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Interfaccia principale:
|
|
90
|
+
|
|
91
|
+
- `user.id`
|
|
92
|
+
- `user.functions.<functionName>(...args)`
|
|
93
|
+
- `user.mongoClient('mongodb-atlas')`
|
|
94
|
+
- `user.refreshAccessToken()`
|
|
95
|
+
- `user.refreshCustomData()`
|
|
96
|
+
- `user.logOut()`
|
|
97
|
+
|
|
98
|
+
## Funzioni server
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const response = await user.functions.calculateScore({ workspaceId: 'w1' })
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Le risposte sono normalizzate lato client per gestire payload JSON/EJSON.
|
|
105
|
+
|
|
106
|
+
## Mongo service
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const mongo = user.mongoClient('mongodb-atlas')
|
|
110
|
+
const collection = mongo.db('mydb').collection('todos')
|
|
111
|
+
|
|
112
|
+
const one = await collection.findOne({ done: false })
|
|
113
|
+
const many = await collection.find({ done: false })
|
|
114
|
+
|
|
115
|
+
await collection.insertOne({ title: 'Task', createdAt: new Date() })
|
|
116
|
+
await collection.updateOne({ title: 'Task' }, { $set: { done: true } })
|
|
117
|
+
await collection.deleteOne({ title: 'Task' })
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Metodi disponibili su `collection`:
|
|
121
|
+
|
|
122
|
+
- `find(query?, options?)`
|
|
123
|
+
- `findOne(query?, options?)`
|
|
124
|
+
- `insertOne(document, options?)`
|
|
125
|
+
- `updateOne(filter, update, options?)`
|
|
126
|
+
- `updateMany(filter, update, options?)`
|
|
127
|
+
- `deleteOne(filter, options?)`
|
|
128
|
+
- `watch(options?)`
|
|
129
|
+
|
|
130
|
+
## Watch / Change streams
|
|
131
|
+
|
|
132
|
+
`watch()` restituisce un async iterator con reconnect automatico e metodo `close()`.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const stream = collection.watch()
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
for await (const change of stream) {
|
|
139
|
+
console.log(change)
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
stream.close()
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Esempio con filtro Realm-like:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const stream = collection.watch({
|
|
150
|
+
filter: {
|
|
151
|
+
operationType: 'update',
|
|
152
|
+
'fullDocument.type': 'perennial'
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## BSON / EJSON
|
|
158
|
+
|
|
159
|
+
Il client esporta anche:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { BSON, EJSON, ObjectId, ObjectID } from '@flowerforce/flowerbase-client'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Il layer Mongo client serializza query/opzioni con EJSON e deserializza le risposte, così tipi BSON come `ObjectId` e `Date` restano coerenti con l'uso Realm-like.
|
|
166
|
+
|
|
167
|
+
## Sessione
|
|
168
|
+
|
|
169
|
+
La sessione (`accessToken`, `refreshToken`, `userId`) viene salvata con chiave:
|
|
170
|
+
|
|
171
|
+
- `flowerbase:<appId>:session`
|
|
172
|
+
|
|
173
|
+
Storage usato:
|
|
174
|
+
|
|
175
|
+
- `localStorage` se disponibile (browser)
|
|
176
|
+
- memory store fallback (ambienti senza `localStorage`)
|
|
177
|
+
|
|
178
|
+
Su bootstrap dell'app viene tentato un refresh automatico dell'access token usando il refresh token salvato.
|
|
179
|
+
|
|
180
|
+
## Logout
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
await user.logOut()
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Invia `DELETE /auth/session` con refresh token e pulisce la sessione locale.
|
|
187
|
+
|
|
188
|
+
## Tipi esportati
|
|
189
|
+
|
|
190
|
+
- `AppConfig`
|
|
191
|
+
- `CredentialsLike`
|
|
192
|
+
- `UserLike`
|
|
193
|
+
- `MongoClientLike`
|
|
194
|
+
- `CollectionLike`
|
|
195
|
+
- `WatchAsyncIterator`
|
|
196
|
+
|
|
197
|
+
## Build e test (workspace)
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
npm run build --workspace @flowerforce/flowerbase-client
|
|
201
|
+
npm run test --workspace @flowerforce/flowerbase-client
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Oppure dal package:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npm run build
|
|
208
|
+
npm test
|
|
209
|
+
```
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { AppConfig, CredentialsLike, ProfileData, SessionData } from './types';
|
|
2
|
+
import { Credentials } from './credentials';
|
|
3
|
+
import { User } from './user';
|
|
4
|
+
export declare class App {
|
|
5
|
+
private static readonly appCache;
|
|
6
|
+
static readonly Credentials: typeof Credentials;
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly baseUrl: string;
|
|
9
|
+
readonly timeout: number;
|
|
10
|
+
private readonly sessionManager;
|
|
11
|
+
private readonly usersById;
|
|
12
|
+
private readonly sessionsByUserId;
|
|
13
|
+
private usersOrder;
|
|
14
|
+
private readonly profilesByUserId;
|
|
15
|
+
private readonly sessionBootstrapPromise;
|
|
16
|
+
private readonly listeners;
|
|
17
|
+
emailPasswordAuth: {
|
|
18
|
+
registerUser: (input: {
|
|
19
|
+
email: string;
|
|
20
|
+
password: string;
|
|
21
|
+
}) => Promise<unknown>;
|
|
22
|
+
confirmUser: (input: {
|
|
23
|
+
token: string;
|
|
24
|
+
tokenId: string;
|
|
25
|
+
}) => Promise<unknown>;
|
|
26
|
+
resendConfirmationEmail: (input: {
|
|
27
|
+
email: string;
|
|
28
|
+
}) => Promise<unknown>;
|
|
29
|
+
retryCustomConfirmation: (input: {
|
|
30
|
+
email: string;
|
|
31
|
+
}) => Promise<unknown>;
|
|
32
|
+
sendResetPasswordEmail: (input: {
|
|
33
|
+
email: string;
|
|
34
|
+
} | string) => Promise<unknown>;
|
|
35
|
+
callResetPasswordFunction: (input: {
|
|
36
|
+
email: string;
|
|
37
|
+
password: string;
|
|
38
|
+
} | string, passwordOrArg?: string, ...args: unknown[]) => Promise<unknown>;
|
|
39
|
+
resetPassword: (input: {
|
|
40
|
+
token: string;
|
|
41
|
+
tokenId: string;
|
|
42
|
+
password: string;
|
|
43
|
+
}) => Promise<unknown>;
|
|
44
|
+
};
|
|
45
|
+
constructor(idOrConfig: string | AppConfig);
|
|
46
|
+
static getApp(appIdOrConfig: string | AppConfig): App;
|
|
47
|
+
get currentUser(): User | null;
|
|
48
|
+
get allUsers(): Readonly<Record<string, User>>;
|
|
49
|
+
private persistSessionsByUser;
|
|
50
|
+
private persistUsersOrder;
|
|
51
|
+
private touchUser;
|
|
52
|
+
private removeUserFromOrder;
|
|
53
|
+
private setSessionForUser;
|
|
54
|
+
private clearSessionForUser;
|
|
55
|
+
private setCurrentSessionFromOrder;
|
|
56
|
+
private notifyListeners;
|
|
57
|
+
private providerUrl;
|
|
58
|
+
private authUrl;
|
|
59
|
+
private functionsUrl;
|
|
60
|
+
private createSession;
|
|
61
|
+
private bootstrapSessionOnLoad;
|
|
62
|
+
private ensureSessionBootstrapped;
|
|
63
|
+
private setLoggedInUser;
|
|
64
|
+
private getOrCreateUser;
|
|
65
|
+
logIn(credentials: CredentialsLike): Promise<User>;
|
|
66
|
+
switchUser(nextUser: User): void;
|
|
67
|
+
removeUser(user: User): Promise<void>;
|
|
68
|
+
deleteUser(user: User): Promise<void>;
|
|
69
|
+
getSessionOrThrow(userId?: string): SessionData;
|
|
70
|
+
getSession(userId?: string): SessionData | null;
|
|
71
|
+
hasUser(userId: string): boolean;
|
|
72
|
+
getProfileSnapshot(userId: string): ProfileData | undefined;
|
|
73
|
+
postProvider<T = unknown>(path: string, body: unknown): Promise<T>;
|
|
74
|
+
private requestWithAccessToken;
|
|
75
|
+
callFunction(name: string, args: unknown[], userId?: string): Promise<any>;
|
|
76
|
+
callFunctionStreaming(name: string, args: unknown[], userId?: string): Promise<AsyncIterable<Uint8Array>>;
|
|
77
|
+
callService(name: string, args: unknown[], service?: string, userId?: string): Promise<unknown>;
|
|
78
|
+
getProfile(userId?: string): Promise<ProfileData>;
|
|
79
|
+
refreshAccessToken(userId?: string): Promise<string>;
|
|
80
|
+
logoutUser(userId?: string): Promise<void>;
|
|
81
|
+
addListener(callback: () => void): void;
|
|
82
|
+
removeListener(callback: () => void): void;
|
|
83
|
+
removeAllListeners(): void;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|