remult 0.16.3-exp.4 → 0.17.0-exp.1

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.
Files changed (62) hide show
  1. package/README.md +5 -27
  2. package/esm/index.d.ts +2 -1
  3. package/esm/remult-express.d.ts +32 -0
  4. package/esm/remult-express.js +130 -2
  5. package/esm/remult-fastify.js +0 -2
  6. package/esm/server/expressBridge.d.ts +44 -8
  7. package/esm/server/expressBridge.js +27 -14
  8. package/esm/src/column.d.ts +3 -3
  9. package/esm/src/context.d.ts +2 -0
  10. package/esm/src/context.js +11 -0
  11. package/esm/src/data-api.d.ts +6 -2
  12. package/esm/src/data-api.js +17 -4
  13. package/esm/src/data-providers/array-entity-data-provider.js +1 -16
  14. package/esm/src/data-providers/rest-data-provider.d.ts +4 -0
  15. package/esm/src/data-providers/rest-data-provider.js +11 -7
  16. package/esm/src/live-query/EventSourceLiveQuery.d.ts +5 -0
  17. package/esm/src/live-query/EventSourceLiveQuery.js +48 -0
  18. package/esm/src/live-query/EventSourceLiveQueryProvider.d.ts +6 -0
  19. package/esm/src/live-query/EventSourceLiveQueryProvider.js +24 -0
  20. package/esm/src/live-query/LiveQuery.d.ts +68 -0
  21. package/esm/src/live-query/LiveQuery.js +227 -0
  22. package/esm/src/live-query/LiveQueryManager.d.ts +33 -0
  23. package/esm/src/live-query/LiveQueryManager.js +97 -0
  24. package/esm/src/remult-proxy.d.ts +1 -1
  25. package/esm/src/remult-proxy.js +11 -5
  26. package/esm/src/remult3/RepositoryImplementation.d.ts +6 -3
  27. package/esm/src/remult3/RepositoryImplementation.js +33 -12
  28. package/esm/src/remult3/remult3.d.ts +10 -6
  29. package/esm/src/server-action.js +4 -3
  30. package/esm/src/sort.d.ts +1 -0
  31. package/esm/src/sort.js +18 -0
  32. package/index.d.ts +2 -1
  33. package/package.json +1 -1
  34. package/remult-express.d.ts +32 -0
  35. package/remult-express.js +193 -3
  36. package/remult-fastify.js +0 -2
  37. package/server/expressBridge.d.ts +44 -8
  38. package/server/expressBridge.js +37 -14
  39. package/src/column.d.ts +3 -3
  40. package/src/context.d.ts +2 -0
  41. package/src/context.js +11 -0
  42. package/src/data-api.d.ts +6 -2
  43. package/src/data-api.js +23 -15
  44. package/src/data-providers/array-entity-data-provider.js +1 -16
  45. package/src/data-providers/rest-data-provider.d.ts +4 -0
  46. package/src/data-providers/rest-data-provider.js +11 -7
  47. package/src/live-query/EventSourceLiveQuery.d.ts +5 -0
  48. package/src/live-query/EventSourceLiveQuery.js +62 -0
  49. package/src/live-query/EventSourceLiveQueryProvider.d.ts +6 -0
  50. package/src/live-query/EventSourceLiveQueryProvider.js +32 -0
  51. package/src/live-query/LiveQuery.d.ts +68 -0
  52. package/src/live-query/LiveQuery.js +310 -0
  53. package/src/live-query/LiveQueryManager.d.ts +33 -0
  54. package/src/live-query/LiveQueryManager.js +196 -0
  55. package/src/remult-proxy.d.ts +1 -1
  56. package/src/remult-proxy.js +19 -5
  57. package/src/remult3/RepositoryImplementation.d.ts +6 -3
  58. package/src/remult3/RepositoryImplementation.js +63 -32
  59. package/src/remult3/remult3.d.ts +10 -6
  60. package/src/server-action.js +3 -2
  61. package/src/sort.d.ts +1 -0
  62. package/src/sort.js +18 -0
package/README.md CHANGED
@@ -3,7 +3,6 @@
3
3
  <img src="https://github.com/remult/remult/raw/master/docs/.vuepress/public/logo.png" width="140" height="140">
4
4
  </a>
5
5
  <h1>Remult</h1>
6
- <p>A CRUD framework for full-stack TypeScript</p>
7
6
  <a href="https://circleci.com/gh/remult/remult/tree/master" rel="nofollow">
8
7
  <img alt="CircleCI" src="https://circleci.com/gh/remult/remult/tree/master.svg?style=shield">
9
8
  </a>
@@ -25,28 +24,14 @@
25
24
  <a href="https://twitter.com/intent/tweet?text=Remult%20-%20open-source%20CRUD%20framework%20for%20full-stack%20TypeScript%0A%0A%40remultjs%0A&url=https%3A%2F%2Fgithub.com%2Fremult%2Fremult">
26
25
  <img alt="Twitter URL" src="https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Ftext%3DRemult%2520-%2520open-source%2520CRUD%2520framework%2520for%2520full-stack%2520TypeScript%250A%250A%2540remultjs%250A%26url%3Dhttps%253A%252F%252Fgithub.com%252Fremult%252Fremult">
27
26
  </a>
27
+ <a href="https://twitter.com/remultjs" rel="nofollow">
28
+ <img alt="Twitter Follow" src="https://img.shields.io/twitter/url.svg?label=Follow%20%40remultjs&style=social&url=https%3A%2F%2Ftwitter.com%2FRemultJs">
29
+ </a>
28
30
  </div>
29
-
30
- <br/>
31
-
32
- <div align="center">
33
- <a href="#getting-started">Getting Started</a> |
34
- <a href="#documentation">Documentation</a> |
35
- <a href="#example-apps">Example Apps</a>
36
- </div>
37
-
38
31
  <hr/>
39
- <br/>
40
32
 
41
- <a href="https://www.youtube.com/watch?v=rEoScmSVNUE" target="_blank">
42
- <p align="center">
43
- <img src="https://github.com/remult/remult/raw/master/static/images/video-thumbnail.jpg" alt="Video thumbnail" width="320" />
44
- </p>
45
- </a>
46
-
47
- <a href="https://www.youtube.com/watch?v=rEoScmSVNUE" target="_blank">
48
- <p align="center">Watch code demo on YouTube here</p>
49
- </a>
33
+ [Getting Started](#getting-started) | [Documentation](#documentation) |
34
+ [Example Apps](#example-apps)
50
35
 
51
36
  ## What is Remult?
52
37
 
@@ -54,13 +39,6 @@
54
39
  as a single source of truth for your API, frontend type-safe API client and
55
40
  backend ORM**.
56
41
 
57
- * :zap: Zero-boilerplate CRUD API routes with paging, sorting, and filtering for Express / Fastify / Next.js / NestJS / Koa / others...
58
- * :ok_hand: Fullstack type-safety for API queries, mutations and RPC, without code generation
59
- * :sparkles: Input validation, defined once, runs both on the backend and on the frontend for best UX
60
- * :lock: Fine-grained code-based API authorization
61
- * :relieved: Incrementally adoptable
62
- * :rocket: Production ready
63
-
64
42
  ### Status
65
43
 
66
44
  Remult is **production-ready** and, in fact, used in production apps since 2018.
package/esm/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export { Field, Fields, StringFieldOptions, FieldsMetadata, Entity, EntityBase, ControllerBase, FieldRef, IdFieldRef, FieldsRef, EntityMetadata, EntityOrderBy, EntityFilter, FindOptions, QueryResult, QueryOptions, Repository, FieldType, FindFirstOptions, ComparisonValueFilter, ValueFilter, IdFilter, ContainsStringValueFilter, getFields, EntityRef, getEntityRef, SortSegments, ValueListFieldType, getValueList, ValueListFieldOptions, ValueListInfo, OmitEB, Paginator, CaptionTransformer } from './src/remult3';
1
+ export { Field, Fields, FieldsMetadata, Entity, EntityBase, ControllerBase, FieldRef, IdFieldRef, FieldsRef, EntityMetadata, EntityOrderBy, EntityFilter, FindOptions, QueryResult, QueryOptions, Repository, FieldType, FindFirstOptions, ComparisonValueFilter, ValueFilter, IdFilter, ContainsStringValueFilter, getFields, EntityRef, getEntityRef, SortSegments, ValueListFieldType, getValueList, ValueListFieldOptions, ValueListInfo, OmitEB, Paginator, CaptionTransformer } from './src/remult3';
2
+ export { StringFieldOptions } from './src/remult3/RepositoryImplementation';
2
3
  export { describeClass } from './src/remult3/DecoratorReplacer';
3
4
  export { EntityOptions } from './src/entity';
4
5
  export { DataProvider, EntityDataProvider, EntityDataProviderFindOptions, ErrorInfo, RestDataProviderHttpProvider } from './src/data-interfaces';
@@ -1,6 +1,38 @@
1
1
  import * as express from 'express';
2
2
  import { RemultServer, RemultServerOptions } from './server/expressBridge';
3
+ import { Remult } from './src/context';
4
+ import { AMessageChannel, ChannelSubscribe } from './src/live-query/LiveQuery';
5
+ import { ServerEventDispatcher, ServerEventMessage } from './src/live-query/LiveQueryManager';
3
6
  export declare function remultExpress(options?: RemultServerOptions<express.Request> & {
4
7
  bodyParser?: boolean;
5
8
  bodySizeLimit?: string;
9
+ messageChannels?: AMessageChannel<any>[];
6
10
  }): express.RequestHandler & RemultServer;
11
+ export declare class ServerEventsController implements ServerEventDispatcher {
12
+ private channels;
13
+ private messageHistoryLength;
14
+ consoleInfo(): void;
15
+ subscribeToChannel(remult: Remult, info: ChannelSubscribe, res: import('express').Response): void;
16
+ connections: clientConnection[];
17
+ constructor(channels: AMessageChannel<any>[], messageHistoryLength?: number);
18
+ sendQueryMessage({ message, clientId, queryId }: ServerEventMessage): void;
19
+ sendChannelMessage<T>(channel: string, message: any): void;
20
+ openHttpServerStream(req: import('express').Request, res: import('express').Response): clientConnection;
21
+ messages: {
22
+ id: number;
23
+ message: any;
24
+ eventType: string;
25
+ }[];
26
+ SendMessage(message: any, eventType?: string): void;
27
+ }
28
+ declare class clientConnection {
29
+ response: import('express').Response;
30
+ clientId: string;
31
+ channels: Record<string, boolean>;
32
+ close(): void;
33
+ closed: boolean;
34
+ write(id: number, message: any, eventType: string): void;
35
+ constructor(response: import('express').Response, clientId: string);
36
+ sendLiveMessage(): void;
37
+ }
38
+ export {};
@@ -1,6 +1,8 @@
1
1
  import * as express from 'express';
2
2
  import { createRemultServer } from './server/expressBridge';
3
- import { initAsyncHooks } from './server/initAsyncHooks';
3
+ import { streamUrl } from './src/live-query/LiveQuery';
4
+ import { LiveQueryManager } from './src/live-query/LiveQueryManager';
5
+ import { remult } from './src/remult-proxy';
4
6
  export function remultExpress(options) {
5
7
  let app = express.Router();
6
8
  if (!options) {
@@ -13,9 +15,21 @@ export function remultExpress(options) {
13
15
  app.use(express.json({ limit: options.bodySizeLimit }));
14
16
  app.use(express.urlencoded({ extended: true, limit: options.bodySizeLimit }));
15
17
  }
16
- initAsyncHooks();
17
18
  const server = createRemultServer(options);
18
19
  server.registerRouter(app);
20
+ let httpServerEvents = new ServerEventsController(options.messageChannels);
21
+ server.liveQueryManager = new LiveQueryManager(httpServerEvents);
22
+ const streamPath = options.rootPath + '/' + streamUrl;
23
+ app.get(streamPath, (req, res) => {
24
+ httpServerEvents.openHttpServerStream(req, res);
25
+ });
26
+ app.post(streamPath, (r, res, next) => server.withRemult(r, res, next), (req, res) => {
27
+ httpServerEvents.subscribeToChannel(remult, req.body, res);
28
+ });
29
+ app.get(streamPath + '/stats', (req, res) => {
30
+ httpServerEvents.consoleInfo();
31
+ res.json("ok");
32
+ });
19
33
  return Object.assign(app, {
20
34
  getRemult: (req) => server.getRemult(req),
21
35
  openApiDoc: (options) => server.openApiDoc(options),
@@ -23,3 +37,117 @@ export function remultExpress(options) {
23
37
  withRemult: (...args) => server.withRemult(...args)
24
38
  });
25
39
  }
40
+ export class ServerEventsController {
41
+ constructor(channels, messageHistoryLength = 1000) {
42
+ this.channels = channels;
43
+ this.messageHistoryLength = messageHistoryLength;
44
+ this.connections = [];
45
+ this.messages = [];
46
+ if (channels) {
47
+ for (const c of channels) {
48
+ c.dispatcher = this;
49
+ }
50
+ }
51
+ }
52
+ consoleInfo() {
53
+ console.log(this.connections.map(x => ({
54
+ client: x.clientId,
55
+ channels: x.channels
56
+ })));
57
+ }
58
+ subscribeToChannel(remult, info, res) {
59
+ let ok = false;
60
+ if (this.channels)
61
+ for (const sc of this.connections) {
62
+ if (sc.clientId === info.clientId) {
63
+ for (const c of this.channels) {
64
+ if (c.userCanSubscribe(info.channel, remult)) {
65
+ sc.channels[info.channel] = !info.remove;
66
+ ok = true;
67
+ }
68
+ }
69
+ }
70
+ }
71
+ if (ok)
72
+ res.json("ok");
73
+ else
74
+ res.status(404).json(`Channel "${info.channel}" not found`);
75
+ }
76
+ sendQueryMessage({ message, clientId, queryId }) {
77
+ for (const sc of this.connections) {
78
+ if (sc.clientId === clientId) {
79
+ sc.write(undefined, message, queryId);
80
+ }
81
+ }
82
+ }
83
+ sendChannelMessage(channel, message) {
84
+ for (const sc of this.connections) {
85
+ if (sc.channels[channel]) {
86
+ sc.write(undefined, message, channel);
87
+ }
88
+ }
89
+ }
90
+ openHttpServerStream(req, res) {
91
+ res.writeHead(200, {
92
+ 'Content-Type': 'text/event-stream',
93
+ 'Cache-Control': 'no-cache',
94
+ 'Connection': 'keep-alive'
95
+ });
96
+ const cc = new clientConnection(res, req.headers["client-id"] || req.query['id']);
97
+ const lastEventId = req.headers['last-event-id'];
98
+ if (lastEventId) {
99
+ this.messages.filter(x => x.id > +lastEventId).forEach(m => cc.write(m.id, m.message, m.eventType));
100
+ }
101
+ this.connections.push(cc);
102
+ req.on("close", () => {
103
+ cc.close();
104
+ this.connections = this.connections.filter(s => s !== cc);
105
+ });
106
+ return cc;
107
+ }
108
+ SendMessage(message, eventType = '') {
109
+ let z = this;
110
+ let id = i++;
111
+ z.messages.push({ id, message: message, eventType });
112
+ while (z.messages.length > z.messageHistoryLength)
113
+ z.messages.shift();
114
+ //console.log({ sendingTo: z.connections.length })
115
+ z.connections.forEach(y => y.write(id, message, eventType));
116
+ }
117
+ }
118
+ class clientConnection {
119
+ constructor(response, clientId) {
120
+ this.response = response;
121
+ this.clientId = clientId;
122
+ this.channels = {};
123
+ this.closed = false;
124
+ //console.log("open connection");
125
+ response.write("event:authenticate\ndata:" + "key" + "\n\n");
126
+ this.sendLiveMessage();
127
+ }
128
+ close() {
129
+ //console.log("close connection");
130
+ this.closed = true;
131
+ }
132
+ write(id, message, eventType) {
133
+ let event = "event:message";
134
+ if (id != undefined)
135
+ event += "\nid:" + id;
136
+ this.response.write(event + "\ndata:" + JSON.stringify({ event: eventType, data: message }) + "\n\n");
137
+ let r = this.response;
138
+ if (r.flush)
139
+ r.flush();
140
+ }
141
+ sendLiveMessage() {
142
+ if (this.closed)
143
+ return;
144
+ this.response.write("event:keep-alive\ndata:\n\n");
145
+ let r = this.response;
146
+ if (r.flush)
147
+ r.flush();
148
+ setTimeout(() => {
149
+ this.sendLiveMessage();
150
+ }, 45000);
151
+ }
152
+ }
153
+ let i = 0;
@@ -1,5 +1,4 @@
1
1
  import { createRemultServer } from './server/expressBridge';
2
- import { initAsyncHooks } from './server/initAsyncHooks';
3
2
  export function remultFastify(options) {
4
3
  function fastifyHandler(handler) {
5
4
  const response = (req, res) => {
@@ -19,7 +18,6 @@ export function remultFastify(options) {
19
18
  };
20
19
  return response;
21
20
  }
22
- initAsyncHooks();
23
21
  const api = createRemultServer(options);
24
22
  const pluginFunction = async (instance, op) => {
25
23
  //@ts-ignore
@@ -1,9 +1,11 @@
1
- import { queuedJobInfoResponse } from '../src/server-action';
1
+ import { ActionInterface, queuedJobInfoResponse } from '../src/server-action';
2
2
  import { DataProvider } from '../src/data-interfaces';
3
- import { Remult, UserInfo } from '../src/context';
3
+ import { DataApi, DataApiRequest, DataApiResponse } from '../src/data-api';
4
+ import { AllowedForInstance, Remult, UserInfo } from '../src/context';
4
5
  import { ClassType } from '../classType';
5
6
  import { Repository } from '../src/remult3';
6
7
  import { IdEntity } from '../src/id-entity';
8
+ import { LiveQueryManager } from '../src/live-query/LiveQueryManager';
7
9
  export interface RemultServerOptions<RequestType extends GenericRequest> {
8
10
  /** Sets a database connection for Remult.
9
11
  *
@@ -57,12 +59,39 @@ export interface GenericResponse {
57
59
  status(statusCode: number): GenericResponse;
58
60
  end(): any;
59
61
  }
60
- export declare class RemultAsyncLocalStorage {
61
- private readonly remultObjectStorage;
62
- constructor(remultObjectStorage: import('async_hooks').AsyncLocalStorage<Remult>);
63
- run(remult: Remult, callback: VoidFunction): void;
64
- getRemult(): Remult;
65
- static instance: RemultAsyncLocalStorage;
62
+ export declare class RemultServerImplementation implements RemultServer {
63
+ queue: inProcessQueueHandler;
64
+ options: RemultServerOptions<GenericRequest>;
65
+ dataProvider: DataProvider | Promise<DataProvider>;
66
+ constructor(queue: inProcessQueueHandler, options: RemultServerOptions<GenericRequest>, dataProvider: DataProvider | Promise<DataProvider>);
67
+ liveQueryManager: LiveQueryManager;
68
+ withRemult<T>(req: GenericRequest, res: GenericResponse, next: VoidFunction): void;
69
+ routeImpl: RouteImplementation;
70
+ handle(req: GenericRequest, gRes?: GenericResponse): Promise<ServerHandleResponse>;
71
+ registeredRouter: boolean;
72
+ registerRouter(r: GenericRouter): void;
73
+ add(key: string, dataApiFactory: ((req: Remult) => DataApi), r: GenericRouter): void;
74
+ process(what: (remult: Remult, myReq: DataApiRequest, myRes: DataApiResponse, origReq: GenericRequest) => Promise<void>): (req: GenericRequest, res: GenericResponse) => Promise<void>;
75
+ getRemult(req: GenericRequest): Promise<Remult>;
76
+ hasQueue: boolean;
77
+ addAction(action: ActionInterface, r: GenericRouter): void;
78
+ openApiDoc(options: {
79
+ title: string;
80
+ version?: string;
81
+ }): any;
82
+ backendMethodsOpenApi: {
83
+ path: string;
84
+ allowed: AllowedForInstance<any>;
85
+ tag: string;
86
+ }[];
87
+ }
88
+ declare class inProcessQueueHandler {
89
+ private storage;
90
+ constructor(storage: QueueStorage);
91
+ submitJob(url: string, req: Remult, body: any): Promise<string>;
92
+ mapQueuedAction(url: string, what: (data: any, r: Remult, res: ApiActionResponse) => void): void;
93
+ actions: Map<string, (data: any, r: Remult, res: ApiActionResponse) => void>;
94
+ getJobInfo(queuedJobId: string): Promise<queuedJobInfo>;
66
95
  }
67
96
  export interface queuedJobInfo {
68
97
  info: queuedJobInfoResponse;
@@ -88,6 +117,12 @@ export declare class EntityQueueStorage implements QueueStorage {
88
117
  getJobInfo(queuedJobId: string): Promise<queuedJobInfo>;
89
118
  createJob(url: string, userId: string): Promise<string>;
90
119
  }
120
+ declare class RouteImplementation {
121
+ map: Map<string, Map<string, GenericRequestHandler>>;
122
+ route(path: string): SpecificRoute;
123
+ handle(req: GenericRequest, gRes?: GenericResponse): Promise<ServerHandleResponse | undefined>;
124
+ middleware(req: GenericRequest, res: GenericResponse, next: VoidFunction): void;
125
+ }
91
126
  export declare class JobsInQueueEntity extends IdEntity {
92
127
  userId: string;
93
128
  url: string;
@@ -98,3 +133,4 @@ export declare class JobsInQueueEntity extends IdEntity {
98
133
  error: boolean;
99
134
  progress: number;
100
135
  }
136
+ export {};
@@ -30,11 +30,30 @@ export function createRemultServer(options) {
30
30
  return dp;
31
31
  return new (await import('./JsonEntityFileStorage')).JsonFileDataProvider('./db');
32
32
  });
33
+ if (!remultObjectStorage) {
34
+ dataProvider = dataProvider.then(async (dp) => {
35
+ try {
36
+ const asyncHooks = await import('async_hooks');
37
+ remultObjectStorage = new RemultAsyncLocalStorage(new asyncHooks.AsyncLocalStorage());
38
+ }
39
+ catch {
40
+ remultObjectStorage = new RemultAsyncLocalStorage(undefined);
41
+ }
42
+ remult.remultFactory = () => {
43
+ const r = remultObjectStorage.getRemult();
44
+ if (r)
45
+ return r;
46
+ else
47
+ throw new Error("remult object was requested outside of a valid context, try running it within initApi or a remult request cycle");
48
+ };
49
+ return dp;
50
+ });
51
+ }
33
52
  if (options.initApi) {
34
53
  dataProvider = dataProvider.then(async (dp) => {
35
54
  var remult = new Remult(dp);
36
55
  await new Promise((res) => {
37
- RemultAsyncLocalStorage.instance.run(remult, async () => {
56
+ remultObjectStorage.run(remult, async () => {
38
57
  await options.initApi(remult);
39
58
  res({});
40
59
  });
@@ -59,7 +78,7 @@ export function createRemultServer(options) {
59
78
  return bridge;
60
79
  }
61
80
  ;
62
- export class RemultAsyncLocalStorage {
81
+ class RemultAsyncLocalStorage {
63
82
  constructor(remultObjectStorage) {
64
83
  this.remultObjectStorage = remultObjectStorage;
65
84
  }
@@ -71,20 +90,13 @@ export class RemultAsyncLocalStorage {
71
90
  }
72
91
  getRemult() {
73
92
  if (!this.remultObjectStorage) {
74
- throw new Error("can't use static remult in this environment, `async_hooks` were not found");
93
+ throw "can't use static remult in this environment, `async_hooks` were not found";
75
94
  }
76
95
  return this.remultObjectStorage.getStore();
77
96
  }
78
97
  }
79
- RemultAsyncLocalStorage.instance = new RemultAsyncLocalStorage(undefined);
80
- remult.remultFactory = () => {
81
- const r = RemultAsyncLocalStorage.instance.getRemult();
82
- if (r)
83
- return r;
84
- else
85
- throw new Error("remult object was requested outside of a valid context, try running it within initApi or a remult request cycle");
86
- };
87
- class RemultServerImplementation {
98
+ let remultObjectStorage;
99
+ export class RemultServerImplementation {
88
100
  constructor(queue, options, dataProvider) {
89
101
  this.queue = queue;
90
102
  this.options = options;
@@ -143,7 +155,7 @@ class RemultServerImplementation {
143
155
  let key = getEntityKey(e);
144
156
  if (key != undefined)
145
157
  this.add(key, c => {
146
- return new DataApi(c.repo(e), c);
158
+ return new DataApi(c.repo(e), c, this.liveQueryManager);
147
159
  }, r);
148
160
  });
149
161
  }
@@ -168,9 +180,10 @@ class RemultServerImplementation {
168
180
  let myReq = new ExpressRequestBridgeToDataApiRequest(req);
169
181
  let myRes = new ExpressResponseBridgeToDataApiResponse(res, req);
170
182
  let remult = new Remult();
183
+ remult._changeListener = this.liveQueryManager;
171
184
  remult.dataProvider = (await this.dataProvider);
172
185
  await new Promise(res => {
173
- RemultAsyncLocalStorage.instance.run(remult, async () => {
186
+ remultObjectStorage.run(remult, async () => {
174
187
  if (req) {
175
188
  let user;
176
189
  if (this.options.getUser)
@@ -1,7 +1,7 @@
1
1
  import { ClassType } from '../classType';
2
2
  import { FieldMetadata, FieldOptions, ValueConverter } from './column-interfaces';
3
3
  import { Filter } from './filter/filter-interfaces';
4
- import { EntityFilter, FindOptions, Repository, RepositoryImplementation } from './remult3';
4
+ import { EntityFilter, FindOptions, idType, Repository, RepositoryImplementation } from './remult3';
5
5
  export declare function makeTitle(name: string): string;
6
6
  export declare class CompoundIdField implements FieldMetadata<string> {
7
7
  fields: FieldMetadata[];
@@ -25,12 +25,12 @@ export declare class CompoundIdField implements FieldMetadata<string> {
25
25
  }
26
26
  export declare class LookupColumn<T> {
27
27
  private repository;
28
- id: string;
28
+ id: idType<T>;
29
29
  setId(val: any): void;
30
30
  waitLoadOf(id: any): Promise<T>;
31
31
  get(id: any): any;
32
32
  set(item: T): void;
33
- constructor(repository: RepositoryImplementation<T>, id: string);
33
+ constructor(repository: RepositoryImplementation<T>, id: idType<T>);
34
34
  get item(): T;
35
35
  waitLoad(): Promise<T>;
36
36
  }
@@ -2,6 +2,7 @@ import { DataProvider, RestDataProviderHttpProvider } from "./data-interfaces";
2
2
  import { RestDataProviderHttpProviderUsingFetch } from './data-providers/rest-data-provider';
3
3
  import { EntityMetadata, EntityRef, FindOptions, Repository } from "./remult3";
4
4
  import { ClassType } from "../classType";
5
+ import { LiveQueryClient } from "./live-query/LiveQuery";
5
6
  export interface ExternalHttpProvider {
6
7
  post(url: string, data: any): Promise<any> | {
7
8
  toPromise(): Promise<any>;
@@ -65,6 +66,7 @@ export declare class Remult {
65
66
  call<T extends ((...args: any[]) => Promise<any>)>(backendMethod: T, classInstance?: any, ...args: GetArguments<T>): ReturnType<T>;
66
67
  /** The current data provider */
67
68
  dataProvider: DataProvider;
69
+ liveQueryProvider: LiveQueryClient;
68
70
  /** A helper callback that can be used to debug and trace all find operations. Useful in debugging scenarios */
69
71
  static onFind: (metadata: EntityMetadata, options: FindOptions<any>) => void;
70
72
  clearAllCache(): any;
@@ -1,6 +1,9 @@
1
1
  import { actionInfo, serverActionField } from './server-action';
2
2
  import { RestDataProvider, RestDataProviderHttpProviderUsingFetch } from './data-providers/rest-data-provider';
3
3
  import { RepositoryImplementation } from "./remult3/RepositoryImplementation";
4
+ import { LiveQueryClient } from "./live-query/LiveQuery";
5
+ import { EventSourceLiveQueryProvider } from "./live-query/EventSourceLiveQueryProvider";
6
+ import { RemultProxy } from "./remult-proxy";
4
7
  export class HttpProviderBridgeToRestDataProviderHttpProvider {
5
8
  constructor(http) {
6
9
  this.http = http;
@@ -95,8 +98,14 @@ export class Remult {
95
98
  constructor(provider) {
96
99
  /* @internal */
97
100
  this.repCache = new Map();
101
+ /*@internal*/
102
+ this._changeListener = {
103
+ saved: (ref) => { },
104
+ deleted: (ref) => { }
105
+ };
98
106
  /** The current data provider */
99
107
  this.dataProvider = new RestDataProvider(() => this.apiClient);
108
+ this.liveQueryProvider = new LiveQueryClient(new EventSourceLiveQueryProvider());
100
109
  this.context = {};
101
110
  this.apiClient = {
102
111
  url: '/api'
@@ -187,6 +196,7 @@ export class Remult {
187
196
  else
188
197
  return this.isAllowed(allowed);
189
198
  }
199
+ //@ts-ignore // type error of typescript regarding args that doesn't appear in my normal development
190
200
  call(backendMethod, classInstance, ...args) {
191
201
  const z = (backendMethod[serverActionField]);
192
202
  if (!z.doWork)
@@ -200,6 +210,7 @@ export class Remult {
200
210
  }
201
211
  /** A helper callback that can be used to debug and trace all find operations. Useful in debugging scenarios */
202
212
  Remult.onFind = (metadata, options) => { };
213
+ RemultProxy.defaultRemult = new Remult();
203
214
  ;
204
215
  export const allEntities = [];
205
216
  export const classHelpers = new Map();
@@ -1,10 +1,14 @@
1
1
  import { Remult } from './context';
2
- import { Repository } from './remult3';
2
+ import { FindOptions, Repository } from './remult3';
3
3
  import { ErrorInfo } from './data-interfaces';
4
+ export interface LiveQueryProvider {
5
+ subscribe(repo: Repository<any>, id: string, options: FindOptions<any>, remult: Remult, ids: any[]): string;
6
+ }
4
7
  export declare class DataApi<T = any> {
5
8
  private repository;
6
9
  private remult;
7
- constructor(repository: Repository<T>, remult: Remult);
10
+ private liveQueryProvider?;
11
+ constructor(repository: Repository<T>, remult: Remult, liveQueryProvider?: LiveQueryProvider);
8
12
  httpGet(res: DataApiResponse, req: DataApiRequest): Promise<void>;
9
13
  httpPost(res: DataApiResponse, req: DataApiRequest, body: any): Promise<void>;
10
14
  static defaultGetLimit: number;
@@ -1,9 +1,10 @@
1
1
  import { customUrlToken, buildFilterFromRequestParameters } from './filter/filter-interfaces';
2
2
  import { Filter } from './filter/filter-interfaces';
3
3
  export class DataApi {
4
- constructor(repository, remult) {
4
+ constructor(repository, remult, liveQueryProvider) {
5
5
  this.repository = repository;
6
6
  this.remult = remult;
7
+ this.liveQueryProvider = liveQueryProvider;
7
8
  }
8
9
  httpGet(res, req) {
9
10
  if (req.get("__action") == "count") {
@@ -13,7 +14,10 @@ export class DataApi {
13
14
  return this.getArray(res, req);
14
15
  }
15
16
  httpPost(res, req, body) {
16
- switch (req.get("__action")) {
17
+ const action = req?.get("__action");
18
+ if (action?.startsWith("subscribe"))
19
+ return this.getArray(res, req, body);
20
+ switch (action) {
17
21
  case "get":
18
22
  return this.getArray(res, req, body);
19
23
  case "count":
@@ -89,10 +93,19 @@ export class DataApi {
89
93
  findOptions.limit = limit;
90
94
  findOptions.page = +request.get("_page");
91
95
  }
92
- await this.repository.find(findOptions)
96
+ const action = request?.get("__action");
97
+ const r = await this.repository.find(findOptions)
93
98
  .then(async (r) => {
94
- response.success(await Promise.all(r.map(async (y) => this.repository.getEntityRef(y).toApiJson())));
99
+ return await Promise.all(r.map(async (y) => this.repository.getEntityRef(y).toApiJson()));
95
100
  });
101
+ if (this.liveQueryProvider && action?.startsWith("subscribe")) {
102
+ response.success({
103
+ id: this.liveQueryProvider.subscribe(this.repository, action.split('|')[1], findOptions, this.remult, r.map(y => this.repository.getEntityRef(y).getId())),
104
+ result: r
105
+ });
106
+ return;
107
+ }
108
+ response.success(r);
96
109
  }
97
110
  catch (err) {
98
111
  response.error(err);
@@ -66,22 +66,7 @@ export class ArrayEntityDataProvider {
66
66
  }
67
67
  if (options.orderBy) {
68
68
  rows = rows.sort((a, b) => {
69
- let r = 0;
70
- for (let i = 0; i < options.orderBy.Segments.length; i++) {
71
- let seg = options.orderBy.Segments[i];
72
- let left = a[seg.field.key];
73
- let right = b[seg.field.key];
74
- if (left > right)
75
- r = 1;
76
- else if (left < right)
77
- r = -1;
78
- if (r != 0) {
79
- if (seg.isDescending)
80
- r *= -1;
81
- return r;
82
- }
83
- }
84
- return r;
69
+ return options.orderBy.compare(a, b);
85
70
  });
86
71
  }
87
72
  rows = pageArray(rows, options);
@@ -19,6 +19,10 @@ export declare class RestEntityDataProvider implements EntityDataProvider {
19
19
  translateToJson(row: any): {};
20
20
  count(where: Filter): Promise<number>;
21
21
  find(options: EntityDataProviderFindOptions): Promise<Array<any>>;
22
+ buildFindRequest(options: EntityDataProviderFindOptions): {
23
+ filterObject: any;
24
+ url: UrlBuilder;
25
+ };
22
26
  update(id: any, data: any): Promise<any>;
23
27
  delete(id: any): Promise<void>;
24
28
  insert(data: any): Promise<any>;
@@ -55,6 +55,16 @@ export class RestEntityDataProvider {
55
55
  return this.http().get(url.url).then(r => +(r.count));
56
56
  }
57
57
  find(options) {
58
+ let { filterObject, url } = this.buildFindRequest(options);
59
+ if (filterObject) {
60
+ url.add("__action", "get");
61
+ return this.http().post(url.url, filterObject).then(x => x.map(y => this.translateFromJson(y)));
62
+ }
63
+ else
64
+ return this.http().get(url.url).then(x => x.map(y => this.translateFromJson(y)));
65
+ ;
66
+ }
67
+ buildFindRequest(options) {
58
68
  let url = new UrlBuilder(this.url());
59
69
  let filterObject;
60
70
  if (options) {
@@ -82,13 +92,7 @@ export class RestEntityDataProvider {
82
92
  if (options.page)
83
93
  url.add('_page', options.page);
84
94
  }
85
- if (filterObject) {
86
- url.add("__action", "get");
87
- return this.http().post(url.url, filterObject).then(x => x.map(y => this.translateFromJson(y)));
88
- }
89
- else
90
- return this.http().get(url.url).then(x => x.map(y => this.translateFromJson(y)));
91
- ;
95
+ return { filterObject, url };
92
96
  }
93
97
  update(id, data) {
94
98
  let result = {};
@@ -0,0 +1,5 @@
1
+ import { LiveQueryClient } from './LiveQuery';
2
+ export declare class EventSourceLiveQuery extends LiveQueryClient {
3
+ private wrapMessage?;
4
+ constructor(wrapMessage?: (what: () => void) => void);
5
+ }