@giosg/realtime-sdk
v2.5.0
Published
SDK for streaming Giosg resources in real-time
Downloads
269
Readme
RealtimeSDK
RealtimeSDK is a JavaScript library, written in TypeScript, that combines the new giosg v5 API endpoints and the channel broadcasting system together. It allows you to observe RESTful resources and resource collections, automatically updating their state in real-time. The observing is done with Observables
from RxJS library.
Core features
- Handles all the state for remote resources and collections! UI components can directly just subscribe resources they are interested in.
- Directly maps to the RESTful API (v5)! Everything available from the v5 APIs can be used!
- Synchronizes trustfully real-time changes: 90% of all RESTful resources and collections support real-time changes out-of-the-box. RealtimeSDK handles all the WebSocket connections and handling of the real-time messaging. It also handles many possible race conditions!
- Optimized to avoid unnecessary network traffic by caching the resource states! If multiple UI components are interested in the same resources, they are only fetched once!
- Memory-efficient: only keeps in memory what is subscribed by UI components. Once completely unsubscribed, the resources are forgotten from the memory.
- Error recovery: ensures that everything is kept up-to-date even after a network issue! Recoverable errors are not even exposed to UI components.
- Resource manipulation synchronization: any creations, updates, and deletions are automatically reflected to the UI!
- Optimistic updates: for the best user experience, update and delete actions are reflected to the UI immediately, before the operation is ready! Only after something goes wrong, the state is rolled back and an error is triggered.
Authentication
RealtimeSDK manages authentication of the user or visitor that is currently using the web app. The recommended way to authorize users is to use OpenID Connect (OAuth 2) authentication. Alternatively, for visitors, there is an AJAX-based authentication method (which will be replaced with a better method in the future).
Authentication with OpenID Connect (OAuth 2)
To get the RealtimeSDK to work on a web app you need to import the library, instantiate the required objects, and connect them to an OpenID Connect compatible authorization endpoint.
import { RealtimeSDK } from '@giosg/realtime-sdk';
import { OpenIDAuthorizer } from '@giosg/realtime-sdk';
// Instantiate the SDK
const sdk = new RealtimeSDK();
// Set up the authentication instance depending on the desired method
const authorizer = new OpenIdAuthorizer({
// The OAuth authorization endpoint URL
authUrl: 'https://service.giosg.com/identity/authorize',
// The `client_id` as specified in OAuth
clientId: 'chats.giosg.com',
// Prefix for the keys when storing authentication infor to session storage
storagePrefix: 'giosg-auth',
// Scopes required by the app, as specified in OAuth
scopes: [
'openid',
'profile',
'email',
/* and then any number of following: 'settings', 'reports', 'users' */
],
// Wait at least half of the expiration time of the token before renewal
minRenewalFactor: 0.5,
// Wait at most 3/4 of the expiration time of the token before renewal
maxRenewalFactor: 0.75,
});
// When your app starts, call this to start the authentication process and socket connections
sdk.connect(authorizer);
You can immediately start subscribing resources and collections after the SDK has been instantiated. HTTP requests and socket connections are only done after connect
has been called!
When running unit tests, you should probably call setAuthentication
method instead of connect
. See "Authentication in tests" section for more details.
Authentication with AJAX
import { RealtimeSDK } from '@giosg/realtime-sdk';
import { AjaxAuthorizer } from '@giosg/realtime-sdk';
// Instantiate the SDK
const sdk = new RealtimeSDK();
// The ID of the organization, who owns this app and the visitor data
const orgId = 'ebd8dc9d-483c-49ad-9bb8-4a80f3e2c281'
// Set up the authentication for the SDK using a *deprecated* AJAX API endpoint
const authorizer = new AjaxAuthorizer(`/api/v5/public/orgs/${orgId}/auth`, {
visitor_global_id: localStorage.getItem('visitor_global_id'),
visitor_secret_id: localStorage.getItem('visitor_secret_id'),
});
// When your app starts, call this to start the authentication process and socket connections
sdk.connect(authorizer);
For now, there is an alternative way of authenticating the user with an AJAX call to a deprecated API endpoint.
Currently, this is the only way to authenticate visitors. Do not use this method for users but use OpenID Connect (OAuth2) instead!
Also note that this method only works on service.giosg.com
domain!
- When authenticating a user with cookies, you would like to use an endpoint
/api/v5/auth
. This is DEPRECATED in favor of OpenID Connect! - When authenticating a visitor, you would like to use an endpoint
/api/v5/public/orgs/<organization_id>/auth
. You should provide an object with the second parameter containing thevisitor_global_id
("gid") andvisitor_secret_id
("sgid") that are stored locally.
Observing the authentication
You usually want to do something with the "currently authenticated user". The auth
method returns an Observable that emits the latest authentication information of the user.
sdk.auth().subscribe(auth => {
console.log(`Currently authenticated with user ${auth.user_id}`);
console.log(`The user belongs to organization ${auth.organization_id}`);
});
The Observable won't emit the authentication information until the user is successfully authenticated.
The auth
parameter is an object having the following attributes, based on the information received from the authorization endpoint (e.g. OAuth2):
Attribute | Description
------------------|---------------
organization_id
| The UUID of the organization to which the authentication is related. For authenticated users, this is the organization of the user. For visitors, this is the organiztion to which the visitor ID (CID) relates to.
user_id
| If the authentication is for an authenticated user account, then is will be the UUID of the user
visitor_id
| If the authentication is for an anonymous visitor, then this will be the ID (CID) of the visitor
expires_at
| The ISO formatted date/time when the authentication will expire. SDK will attempt to renew the access token before it expires
access_token
| The access token string that SDK will use internally for all the HTTP requests (with Authorization: Bearer <token>
HTTP header) and WebSocket connection (as token
URL parameter)
See also the example how to stream resources of the authenticated user
Resources
Observe resources
The most important feature of the RealtimeSDK is that you can load the immediate state and then all the future updates for one specific resource. The stream completes only if the resource ever gets deleted.
// Start listening changes to a specific resource
sdk.streamResource('/api/v5/orgs/c0c329ce-bd7f-41e3-b79b-af2f7f9f334b')
.subscribe(organization => {
// Emits the resource immediately when loaded, and later
// all the changes to the resource.
})
;
If connection to the server breaks, the SDK will automatically reload the resource when the connection is re-established, and any missed updates will be emitted!
The stream will result in an error if the immediate load of the resource fails. However, after that the SDK will recover from connection problems automatically, reloading and emitting the resource if necessary.
Stop observing resources
SDK will automatically handle real-time channel listening under-the-hood. The client monitors changes to the resource as long as the user of SDK is being subscribed to a resource stream. Therefore, you should unsubscribe when you do not need it any more, in order to prevent unnecessary network traffic and memory load.
// Start listening changes to the resource
let resourceStream = sdk.streamResource('/api/v5/orgs/c0c329ce-bd7f-41e3-b79b-af2f7f9f334b');
let subscription = resourceStream.subscribe(organization => { /* ... */ });
// You should cancel the subscription when you are not interested in the changes any more
subscription.unsubscribe();
Observe resources of the authenticated user
In many cases you may want to use the information of the authenticated user to determine which resources are observed. A common recipe is to use the observable returned by the auth
method and Observable.switchMap
.
// Observe the details of the current user
sdk.auth()
.map(auth => auth.user_id)
.distinctUntilChanged()
.switchMap(userId => sdk.streamResource('/api/v5/users/' + userId))
.subscribe(user => {
console.log("My name is " + user.full_name);
})
;
// Observe the details of the current user's organization
sdk.auth()
.map(auth => auth.organization_id)
.distinctUntilChanged()
.switchMap(orgId => sdk.streamResource('/api/v5/orgs/' + orgId))
.subscribe(organization => {
console.log("My organization's name is " + organization.name);
})
;
However, there is a shortcut helper method for this named switchUser
:
// Observe the details of the current user
sdk.switchUser(userId => sdk.streamResource('/api/v5/users/' + userId))
.subscribe(user => {
console.log("My name is " + user.full_name);
})
;
// Observe the details of the current user's organization
sdk.switchUser((userId, orgId) => sdk.streamResource('/api/v5/orgs/' + orgId))
.subscribe(organization => {
console.log("My organization's name is " + organization.name);
})
;
Using custom channel when streaming resources
In advanced use, if you want to optimize the number of channel subscriptions when streaming idividual resources, you may additionally define a custom channel from which the resource updates are listened. If not defined, this defaults to the resource URL:
// Start listening changes to a specific resource, using a channel of the parent collection
sdk.streamResource('/api/v5/users/xxxxx/chats/yyyyy', { channel: '/api/v5/users/xxxxx/chats' })
.subscribe(chat => {
// ...
})
;
Load resource without observing changes
In some cases you may be interested in just the current state of the resource but not any future changes. You can do this by calling getResource
method. This just makes a GET request, without listening real-time changes.
Note that this still returns an Observable that needs to be subscribed.
// Start listening changes to a specific resource
sdk.getResource('/api/v5/orgs/c0c329ce-bd7f-41e3-b79b-af2f7f9f334b')
.subscribe(organization => {
// Emits the resource's latest state and then completes.
// Does not emit any further changes to the resource.
}, error => {
// Failed to load the resource.
})
;
This is similar than calling Observable.first method of the Observable returned by streamResource
, but it is more efficient, as it does not use real-time socket connection at all.
In this case, the subscription does not need to be manually unsubscribed, unless you want to abort the request.
Create a new resource
You can get an observable for creating new resource by calling postResource
method. By subscribing to it, the resource will be POSTed to the collection. The Observable emits exactly one value: the created resource object. It then completes. In other words, this does not subscribe to any future changes of the resource.
// Create a new resource by posting to a collection
let userData = {
first_name: "John",
last_name: "Smith",
/* etc... */
};
sdk.postResource('/api/v5/orgs/c0c329ce-bd7f-41e3-b79b-af2f7f9f334b/users', userData)
.subscribe(createdUser => {
// Emits the resource when created successfully. It then completes.
}, error => {
// The creation failed!
})
;
If the creation fails, the observable results in an error.
NOTE: The HTTP request starts only after the observable is actually subscribed. This is because of the anatomy of an observable. In other words, to create a resource, even if you are not interested in the results, you need to call sdk.postResource(...).subscribe();
.
Also, by default it makes a new request for each subscriber. If this behavior is not desired, you can multicast the observable, e.g by calling Observable.share
.
Also note that the SDK will automatically "add" the newly created resource to the collection (see below) it was posted! Therefore, if you are streaming the collection to which te resource was POSTed, then SDK will automatically ensure that the resource is added to the collection (and therefore e.g. shown in the UI).
Update a resource
You can update an existing resource by calling putResource
for a full update or patchResource
for a partial update. Similar to postResource
, this only emits the updated resource and then completes.
// Update a new resource by putting
let updatedUserData = {
first_name: "John",
last_name: "Doe",
/* etc... */
};
sdk.putResource('/api/v5/orgs/c0c329ce-bd7f-41e3-b79b-af2f7f9f334b/users/6437bd00-5c92-4310-b4b6-1cedb35aa3c9', updatedUserData)
.subscribe(updatedUser => {
// Emits the resource when updated successfully. It then completes.
}, error => {
// The update failed!
})
;
NOTE: As with postResource
, these observables needs to be subscribed in order to start the HTTP request, and each subscription results in a new request. See the notes above.
Destroy a resource
You can destroy an existing resource by calling deleteResource
for a resource endpoint. Unlike other methods, this only emits a single undefined value when successful. It does not emit any changes (because the resource does not exist any more).
// Destroy the resource
sdk.deleteResource('/api/v5/orgs/c0c329ce-bd7f-41e3-b79b-af2f7f9f334b/users/6437bd00-5c92-4310-b4b6-1cedb35aa3c9')
.subscribe(() => {
// Destroyed successfully!
}, error => {
// The destroying failed!
})
;
NOTE: As with postResource
, this observable needs to be subscribed in order to start the HTTP request, and each subscription results in a new request. See the notes above.
Collections
Observe collections
Another core feature of the Giosg SDK is that it can be used to observe collections of resources and all the changes made to it in real-time.
The simplest way to observe a collection of resources is to stream arrays of resources.
// Start listening changes to the collection
sdk.streamCollectionArray('/api/v5/orgs/c0c329ce-bd7f-41e3-b79b-af2f7f9f334b/users')
.subscribe(users => {
// Emits the array of ALL the resources in the collection, and then
// all the changed arrays.
})
;
However, you should only use streamCollectionArray
method if you know that your collection is relatively small by its size. This is because all the pagination chunks needs to be loaded from the server before the complete array can be built. For large collections, let's say, more than 100 items, this can be a performance issue, and you should use the streamCollection
method instead, descibed below.
Observe collection resource streams
For large arrays, it is strongly recommended to use lower-level streamCollection
method instead. Instead of arrays, it emits higher-order observables, that is, "observables of observables". The emitted "inner" observables emits all the resources in the collection, in order.
// Start listening changes to the collection
sdk.streamCollection('/api/v5/orgs/c0c329ce-bd7f-41e3-b79b-af2f7f9f334b/users')
.subscribe(userStream => {
// Emits an Observable for the users in the collection, according to their current
// state in the collection. If the contents of the collection is changed, then
// another Observable is emitted.
})
;
Here's an example, how to observe max. 10 manager users, using an endpoint that lists all the users.
// Observe the first 10 manager users from the users collection
sdk.streamCollection('/api/v5/orgs/c0c329ce-bd7f-41e3-b79b-af2f7f9f334b/users')
.switchMap(userStream => {
// Take the 10 first resources matching the giving criteria, ignoring the rest
return userStream
// Include only if the user is a manager
.filter(user => user.is_manager)
// Only max. 10 managers.
// SDK only loads the minimum number of required page chunks from the server.
.take(10)
// Emit as an array
.toArray()
;
})
.subscribe(firstManagers => {
// Emits the first 10 manager users as an array, and then
// a new array whenever the collection changes.
})
;
The collection pagination chunks are loaded lazily on-demand. That is, the pages are only actually loaded from the server if there are any subscribers interested in their contents! For example, you can use Observable.take
to take only the given number of first resources from the collection. The latter pages are not loaded from the server at all, making the operation fast even for very large collections.
Please be careful when working with higher-order observables. It is likely that the underlying collection sometimes changes before a "inner" Observable is completed, making it obsolete. For example, a resource is added to the collection before all the page chunks are loaded from the server. Luckily, Observable has handly methods to manage inner observables! For example, you can use Observable.switchMap which is useful to only use the latest emitted inner stream of resources.
Stop observing collection changes
As with any stream returned by the SDK, you should unsubscribe your subscription when you are not interest in the changes any more!
// Start listening changes to the collection
let collectionStream = sdk.streamCollection('/api/v5/orgs/c0c329ce-bd7f-41e3-b79b-af2f7f9f334b/users');
// Subscribe to the stream
let subscription = collectionStream.subscribe(userStream => { /* ... */ });
// You should cancel the subscription when you are not interested in the changes any more
subscription.unsubscribe();
Load a collection without observing changes
// Returns an observable for the current contents of the collection
sdk.getCollection('/api/v5/orgs/c0c329ce-bd7f-41e3-b79b-af2f7f9f334b/users')
.subscribe(user => {
// Calls this for EACH RESOURCE in the collection and then completes!
}, error => {
// Loading one of the collection page loads failed!
})
;
Sometimes you just want to get all the resources in the collection without observing the changes to the content. In this case you can use getCollection
method that just retrieves the current resources in the collection as a single observable. It emits the items until the end of the collection is reached, and then completes.
The main difference to earlier methods is that the observable emits each resource from the current state of the collection. This is roughly equivalent to: sdk.streamCollection(...).first().mergeAll()
but is more efficient, because it does not use the real-time socket connection at all.
Ordering collections
// Observe the collection in a specific order
let collectionStream = sdk.streamCollection(
'/api/v5/orgs/c0c329ce-bd7f-41e3-b79b-af2f7f9f334b/users',
{ordering: '-created_at'}
)
// Convert to arrays of max. 10 most recently created users
.switchMap(userStream => userStream.take(10).toArray())
.subscribe(recentlyCreatedUsers => {
// Emits max. 10 most recently created users as an array,
// and then a new array whenever a new user is created!
})
;
The streamCollection
, streamCollectionArray
and getCollection
methods accepts additional options. You can use the ordering
option to define the order of the resources in the collection. The option must be supported by the HTTP endpoint.
As with the HTTP endpoint, you may use the -
prefix to reverse the order. You can also separate secondary sorting criterias with commas (,
).
Filtering collections
All the collection streaming and loading methods described above (streamCollection
, streamCollectionArray
and getCollection
) support filtering options:
// Start listening changes to the collection
sdk.streamCollectionArray(
'/api/v5/orgs/c0c329ce-bd7f-41e3-b79b-af2f7f9f334b/users',
{ is_manager: true }
)
.subscribe(managerUsers => {
// Emits the array of filtered resources in the collection
})
;
This will load the resources from the following URL:
/api/v5/orgs/c0c329ce-bd7f-41e3-b79b-af2f7f9f334b/users?is_manager=true
For the best performance, it is recommended that the backend supports the filtering GET parameters. However, the filtering will be also done client-side, in case the backend does not support the filtering parameter.
You may provide multiple filtering criterias, in which case all of them must match.
The following filtering types are supported:
Type | Description
--------|------------
string | Added to the URL request as URI-encoded GET parameter
integer | Added to the URL request as stringified GET parameter
boolean | Added to the URL reqeust as ether true
or false
GET parameter
Note that currently, for simplicity, only exact value filtering is supported with URL GET parameters. Negations or comparisons may be added in the future. You may perform any custom filtering on the client-side. Also, no "or" condition in currently supported.
Observing resource additions
In some cases you may only be interested in resources that are added to a collection. For example, you may want to play a sound or show a small notification whenever a new chat message is received, but you would like to ignore any initial chat messages for this purpose.
In these cases, you can use streamCollectionAdditions
helper method:
// Start listening additions to the collections
sdk.streamCollectionAdditions(
'/api/v5/users/xxxxx/chats/yyyyy/messages',
{ ordering: '-created_at' }
)
.subscribe(newMessage => {
// Called for each chat message that is added to the collection
// while this collection is being subscribed!
})
;
Please note the following characteristics of the returned observable:
- It only emits resources that are added to the collection during the subscription. Any resources existing in the collection when being subscribed are omitted.
- It never completes, but it fails if the loading of resources fail.
- This completely ignores any updates and removals of resources. So, the resources in the collection should be "immutable" as possible.
- If the resource is removed and re-added to a collection later, it will be emitted multiple times. You can avoid this by using, for example,
.distinctKey("id")
, but consider memory usage. - IMPORTANT: Internally, this needs to observe to the whole collection like
streamCollection
does, and therefore you can (and should) provide the same options, including filtering criteria. Theordering
option will affect the order in which any "simulatenously" added resources are emitted. If the ordering does not matter and you are streaming the same collection elsewhere with any specific ordering, using the same ordering with this method will preserve a bunch of unnecessary HTTP requests!
Testing and debugging
Authentication in tests
In unit tests you probably do not want to call connect
method of RealtimeSDK instance, as this tries to redirect the browser or make an AJAX request to the authentication endpoint and create an actual Websocket connection. Instead, you may initialize the SDK with a mock authentication object with setAuthentication
method like this:
import { RealtimeSDK } from '@giosg/realtime-sdk';
const sdk = new RealtimeSDK();
// Set mock authentication for the SDK
sdk.setAuthentication({
access_token: "<MY_ACCESS_TOKEN>",
user_id: "b8aca5a7-4c73-4de1-bcf4-d3614029db1f",
organization_id: "4ee0c512-7fb5-4b00-bfec-a6c6cd7f94e3",
});
It also accepts an Observable that emits authentication objects (to mimic changes in the authentication state):
import { Subject } from 'rxjs/Subject';
// ...
// Set up authentication from an Observable of authentication objects
let mockAuth = new Subject();
sdk.setAuthentication(mockAuth);
mockAuth.next({ access_token: "<FIRST_ACCESS_TOKEN>", /* ... */ });
mockAuth.next({ access_token: "<SECOND_ACCESS_TOKEN>", /* ... */ });
Mocking resources
The has a bunch of methods that allows to to mock the resources and collections. This means than whenever a certain resource or collection is streamed, the "fake" objects are emitted instead.
This is useful for testing components that use SDK, as you do not have to fake HTTP requests or socket traffic at all! You can also use these methods runtime for debugging.
Mock to always return a certain value:
// Mock the resource
sdk.mockResource("/api/v5/users/xxxx", {
id: "xxxx",
first_name: "John",
last_name: "Smith",
// ...
});
It also allows you to mock with an Observable. By using a Subject, you can fake changes to a resource:
// Fake the changes to the resource
let fakeStream = new Subject<Resource>();
sdk.mockResource("/api/v5/users/xxxx", fakeStream);
// ...
fakeStream.next({
id: "xxxx",
is_online: false,
// ...
});
// ...
fakeStream.next({
id: "xxxx",
is_online: true,
// ...
});
You can reset the mocking for the resource, which re-enables retrieving the resource via HTTP and listening changes via socket:
// Reset the mocking for the resource
sdk.mockResource("/api/v5/users/xxxx", null);
Mocking collections
Mock to always return a certain array of resources:
sdk.mockCollectionArray("/api/v5/users", [
{ /* User 1 */ },
{ /* User 2 */ },
{ /* User 3 */ },
]);
You can fake changes to the collection by mocking with an Observable of arrays, for example, by using a Subject. It also allows you to mock with an Observable. By using a Subject, you can fake changes to a resource:
let fakeStream = new Subject<Resource[]>();
sdk.mockCollection("/api/v5/users", fakeStream);
// ...
fakeStream.next([
{ /* User 1 */ },
{ /* User 2 */ },
]);
// ...
fakeStream.next([
{ /* User 1 */ },
{ /* User 2 */ },
{ /* User 3 */ },
]);
Mocking resource actions
You can also mock the responses to POST, PUT, PATCH and DELETE actions done to resources via RealtimeSDK. For POST, PUT and PATCH, you can set a static response object.
// Mock POST response
sdk.mockResourcePost("/api/v5/users", {
id: "xxxx",
first_name: "John",
last_name: "Smith",
// ...
});
// Mock PUT response
sdk.mockResourcePut("/api/v5/users/xxxx", {
id: "xxxx",
first_name: "John",
last_name: "Smith",
// ...
});
// Mock PATCH response
sdk.mockResourcePatch("/api/v5/users/xxxx", {
id: "xxxx",
first_name: "John",
last_name: "Smith",
// ...
});
Alternatively, for faking asyncronous responses, you can provide an Observable, such as a Subject. This applies any of the action mocking methods:
// Mock asyncronous POST response
let fakeResponse = new Subject<Resource>();
sdk.mockResourcePost("/api/v5/users/xxxx", fakeResponse);
// ...
fakeResponse.next({
id: "xxxx",
first_name: "John",
last_name: "Smith",
// ...
});
fakeResponse.complete();
In tests, you can also provide a function as the mock. The function will be called with the payload and the access token. Its return value, which can be an object or an Observable, will be handled similar to previous examples.
// Test that the code POSTs a new resource
let postHandler = jasmine.createSpy('postHandler').and.returnValue({
id: "xxxx",
first_name: "John",
last_name: "Smith",
// ...
});
sdk.mockResourcePost("/api/v5/users", postHandler);
// ...
expect(postHandler).toHaveBeenCalledWith({ /* POSTed object */ }, "<ACCESS_TOKEN>");
Test mode
By default, SDK will only fake resources and collections that are specifically mocked with the mocking methods. Any other resources and collections will be loaded remotely. In tests, however, this is not usually what you want.
You can prevent the SDK to make any actual HTTP requests or socket traffic by switching it to the test mode. For example, in Jasmine tests you can do this:
// Disallow actual network traffic during the test
beforeEach(() => sdk.startTestMode());
afterEach(() => sdk.stopTestMode());
This is recommended to be done in each test that uses an SDK.
Optimistic resource altering
RealtimeSDK supports optimistic resource updates and deletions.
By default, any updates and removals are applied immediately to any related resource or collection stream! The actual operation is then run in background. If the operation fails, then the optimistic changes are rolled back.
NOTE: The support for optimistic additions could be added later.
When using putResource
, patchResource
or deleteResource
, you may optionally define any optimistic side-effects yourself.
sdk.putResource(
`/api/v5/users/${userId}`,
{
first_name: "John",
last_name: "Smith",
// ...
},
{
// Which other resources should be updated?
optimisticUpdates: [{
resourceUrl: `/api/v5/orgs/${organizationId}/users/${userId}`,
resource: {
full_name: "John Smith",
}
}],
// Which resources should be removed from their collections?
optimisticRemovals: [`/api/v5/orgs/${organizationId}/cool_guys/${userId}`],
}
).subscribe();
Publishing
To publish newer version of this package to npm ypu need to:
- update package version in
package.json
- run
npm install
to install all dependencies and updatepackage-lock.json
- login with
npm login --scope=@giosg
to our organization - publish with
npm publish -–access=public