From 3bf87745a6bfaed5b4a3dcb2789f1593d150bdae Mon Sep 17 00:00:00 2001 From: Alexandre Roux Date: Fri, 14 Jun 2024 17:47:48 +0200 Subject: [PATCH] wip: scheduler support for new function interop --- .../src/node/express_http_request_node.dart | 2 +- ...ebase_functions_https_node_js_interop.dart | 148 ++++++++++++++++++ .../lib/src/node/firebase_functions_node.dart | 97 ++++++++---- .../firebase_functions_node_js_interop.dart | 144 ++--------------- ...e_functions_scheduler_node_js_interop.dart | 88 +++++++++++ functions_node/pubspec.yaml | 6 +- 6 files changed, 320 insertions(+), 165 deletions(-) create mode 100644 functions_node/lib/src/node/firebase_functions_https_node_js_interop.dart create mode 100644 functions_node/lib/src/node/firebase_functions_scheduler_node_js_interop.dart diff --git a/functions_node/lib/src/node/express_http_request_node.dart b/functions_node/lib/src/node/express_http_request_node.dart index 15ecf64..639a984 100644 --- a/functions_node/lib/src/node/express_http_request_node.dart +++ b/functions_node/lib/src/node/express_http_request_node.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:tekartik_firebase_functions/firebase_functions.dart'; import 'package:tekartik_firebase_functions_node/src/import_common.dart'; -import 'package:tekartik_firebase_functions_node/src/node/firebase_functions_node_js_interop.dart' +import 'package:tekartik_firebase_functions_node/src/node/firebase_functions_https_node_js_interop.dart' as node; import 'package:tekartik_http/http.dart' as http; diff --git a/functions_node/lib/src/node/firebase_functions_https_node_js_interop.dart b/functions_node/lib/src/node/firebase_functions_https_node_js_interop.dart new file mode 100644 index 0000000..a0ecbd0 --- /dev/null +++ b/functions_node/lib/src/node/firebase_functions_https_node_js_interop.dart @@ -0,0 +1,148 @@ +// Copyright (c) 2018, Anatoly Pulyaevskiy. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:js_interop' as js; + +import 'package:tekartik_firebase_functions_node/src/import_common.dart'; +import 'firebase_functions_node_js_interop.dart'; + +extension type JSHttpsFunctions._(js.JSObject _) implements js.JSObject {} + +extension JSHttpsFunctionsExt on JSHttpsFunctions { + // Handles HTTPS requests. + // + // Signature: + // export declare function onRequest(opts: HttpsOptions, handler: (request: Request, response: express.Response) => void | Promise): HttpsFunction; + + @js.JS('onRequest') + external JSHttpsFunction _onRequest( + JSHttpsOptions options, _JSHttpsHandler handler); + + @js.JS('onRequest') + external JSHttpsFunction _onRequestNoOptions(_JSHttpsHandler handler); + + JSHttpsFunction onRequest( + {JSHttpsOptions? options, required HttpsHandler handler}) { + js.JSAny? jsHandler(JSHttpsRequest request, JSHttpsResponse response) { + var result = handler(request, response); + if (result is Future) { + return result.toJS; + } else { + return null; + } + } + + if (options == null) { + //devPrint('Setting handler no options'); + return _onRequestNoOptions(jsHandler.toJS); + } else { + // devPrint('Setting handler'); + return _onRequest(options, jsHandler.toJS); + } + } +} + +// An express request with the wire format representation of the request body. +// +extension type JSHttpsRequest._(js.JSObject _) implements js.JSObject {} + +extension JSHttpsRequestExt on JSHttpsRequest { + /// From node IncomingMessage + /// Request URL string. This contains only the URL that is present in the actual HTTP request. + /// For example: '/status?name=ryan' + external String get url; + + /// Firebase function extension + /// + /// Buffer The wire format representation of the request body. + /// Buffer is not only and extends Uint8Array + external js.JSUint8Array? get rawBody; + + /// The request method as a string. Read only. Examples: 'GET', 'DELETE'. + external String get method; + + /// The request/response headers object. + /// + /// Key-value pairs of header names and values. Header names are lower-cased. + /// + /// Prints something like: + /// + /// { 'user-agent': 'curl/7.22.0', + /// host: '127.0.0.1:8000', + /// accept: '*/*' } + /// console.log(request.headers); COPY + /// Duplicates in raw headers are handled in the following ways, depending on the header name: + /// + /// Duplicates of age, authorization, content-length, content-type, etag, expires, from, host, if-modified-since, if-unmodified-since, last-modified, location, max-forwards, proxy-authorization, referer, retry-after, server, or user-agent are discarded. To allow duplicate values of the headers listed above to be joined, use the option joinDuplicateHeaders in http.request() and http.createServer(). See RFC 9110 Section 5.3 for more information. + /// set-cookie is always an array. Duplicates are added to the array. + /// For duplicate cookie headers, the values are joined together with ; . + /// For all other headers, the values are joined together with , + external js.JSAny? get headers; +} + +extension type JSHttpsResponse._(js.JSObject _) implements js.JSObject { + /// https://expressjs.com/en/4x/api.html#res.send + /// Sends the HTTP response. + /// The body parameter can be a Buffer object, a String, an object, Boolean, or an Array. For example: + external void send([js.JSAny? body]); + + /// Ends the response process. This method actually comes from Node core, + /// specifically the response.end() method of http.ServerResponse. + external void end(); + + /// Sets the HTTP status for the response. + /// It is a chainable alias of Node’s response.statusCode. + external JSHttpsResponse status(int statusCode); + + /// Appends the specified value to the HTTP response header field. + /// If the header is not already set, it creates the header with the + /// specified value. The value parameter can be a string or an array. + /// + /// Note: calling res.set() after res.append() will reset the previously-set header value. + /// + /// res.append('Link', ['', '']) + /// res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly') + /// res.append('Warning', '199 Miscellaneous warning') + @js.JS('append') + external JSHttpsResponse _setHeader(String field, js.JSAny value); + + JSHttpsResponse setHeader(String field, String value) => + _setHeader(field, value.toJS); + JSHttpsResponse setHeaderList(String field, List values) => + _setHeader(field, values.map((e) => e.toJS).toList().toJS); +} + +extension JSHttpsResponseExt on JSHttpsResponse {} + +typedef JSHttpsFunction = js.JSFunction; +typedef _JSHttpsHandler = js.JSFunction; + +typedef HttpsHandler = FutureOr Function( + JSHttpsRequest request, JSHttpsResponse response); + +typedef JSFirebaseFunction = js.JSFunction; + +/// Options that can be set on an onRequest HTTPS function. +/// +/// export interface HttpsOptions extends Omit +extension type JSHttpsOptions._(js.JSObject _) implements JSGlobalOptions { + /// Options + external factory JSHttpsOptions( + {String? region, + String? memory, + int? concurrency, + js.JSAny? cors, + int? timeoutSeconds}); +} + +extension JSHttpsOptionsExt on JSHttpsOptions { + /// string | boolean | RegExp | Array + /// + /// If true, allows CORS on requests to this function. If this is a string or + /// RegExp, allows requests from domains that match the provided value. If + /// this is an Array, allows requests from domains matching at least one entry + /// of the array. Defaults to true for https.CallableFunction and false + /// otherwise. + external js.JSAny? get cors; +} diff --git a/functions_node/lib/src/node/firebase_functions_node.dart b/functions_node/lib/src/node/firebase_functions_node.dart index 646eda1..4c2b6c8 100644 --- a/functions_node/lib/src/node/firebase_functions_node.dart +++ b/functions_node/lib/src/node/firebase_functions_node.dart @@ -2,8 +2,13 @@ import 'dart:js_interop'; import 'package:tekartik_firebase_functions/firebase_functions.dart'; import 'package:tekartik_firebase_functions_http/firebase_functions_http.dart'; +import 'package:tekartik_firebase_functions_node/src/import_common.dart'; +import 'package:tekartik_firebase_functions_node/src/node/firebase_functions_https_node_js_interop.dart' + as node; import 'package:tekartik_firebase_functions_node/src/node/firebase_functions_node_js_interop.dart' as node; +import 'package:tekartik_firebase_functions_node/src/node/firebase_functions_scheduler_node_js_interop.dart' + as node; import 'express_http_request_node.dart'; @@ -11,6 +16,7 @@ final firebaseFunctionsNode = FirebaseFunctionsNode(); class FirebaseFunctionsNode extends FirebaseFunctionsHttp { final nativeInstance = node.firebaseFunctionsModule; + @override void operator []=(String key, FirebaseFunction function) { nativeInstance[key] = (function as FirebaseFunctionNode).nativeInstance; @@ -18,11 +24,16 @@ class FirebaseFunctionsNode extends FirebaseFunctionsHttp { @override HttpsFunctions get https => HttpsFunctionsNode(this, nativeInstance.https); + + @override + SchedulerFunctions get scheduler => + SchedulerFunctionsNode(this, nativeInstance.scheduler); } class HttpsFunctionsNode with HttpsFunctionsMixin implements HttpsFunctions { final FirebaseFunctionsNode functions; final node.JSHttpsFunctions nativeInstance; + HttpsFunctionsNode(this.functions, this.nativeInstance); @override @@ -45,47 +56,79 @@ class HttpsFunctionsNode with HttpsFunctionsMixin implements HttpsFunctions { //throw UnimplementedError('onRequestV2'); } -} /* +} -class ExpressHttpRequestNode extends ExpressHttpRequestWrapperBase - implements ExpressHttpRequest { - node.HttpsRequest get nativeInstance => - (implHttpRequest as HttpsRequestNode).nodeHttpRequest - as impl.ExpressHttpRequest; +abstract class FirebaseFunctionNode implements FirebaseFunction { + final FirebaseFunctionsNode firebaseFunctionsNode; + + FirebaseFunctionNode(this.firebaseFunctionsNode); - ExpressHttpRequestNode(ExpressHttpRequest httpRequest, Uri rewrittenUri) - : super(HttpRequestNode(httpRequest), rewrittenUri); + node.JSFirebaseFunction get nativeInstance; +} +class HttpsFunctionNode extends FirebaseFunctionNode implements HttpsFunction { @override - dynamic get body => nativeInstance.body; + final node.JSHttpsFunction nativeInstance; - ExpressHttpResponse? _response; + HttpsFunctionNode(super.firebaseFunctionsNode, this.nativeInstance); +} + +node.JSHttpsOptions toNodeHttpsOptions(HttpsOptions httpsOptions) { + return node.JSHttpsOptions( + region: httpsOptions.region, cors: httpsOptions.cors?.toJS); +} +class SchedulerFunctionNode extends FirebaseFunctionNode + implements ScheduleFunction { @override - ExpressHttpResponse get response => _response ??= - ExpressHttpResponseNode(nativeInstance.response as NodeHttpResponse); -}*/ + final node.JSScheduleFunction nativeInstance; -class ExpressHttpResponseNode extends ExpressHttpResponseWrapperBase - implements ExpressHttpResponse { - ExpressHttpResponseNode(super.implHttpResponse); + SchedulerFunctionNode(super.firebaseFunctionsNode, this.nativeInstance); } -class HttpsFunctionNode extends FirebaseFunctionNode implements HttpsFunction { +node.JSScheduleOptions toNodeScheduleOptions(ScheduleOptions options) { + return node.JSScheduleOptions( + region: options.region, + schedule: options.schedule, + timeZone: options.timeZone, + memory: options.memory, + timeoutSeconds: options.timeoutSeconds); +} + +class SchedulerFunctionsNode + with SchedulerFunctionsDefaultMixin + implements SchedulerFunctions { + final FirebaseFunctionsNode functions; + final node.JSSchedulerFunctions nativeInstance; + + SchedulerFunctionsNode(this.functions, this.nativeInstance); + @override - final node.JSHttpsFunction nativeInstance; + ScheduleFunction onSchedule( + ScheduleOptions scheduleOptions, ScheduleHandler handler) { + FutureOr handleRequest(node.JSScheduledEvent data) { + var scheduleEvent = ScheduleEventNode(data); + return handler(scheduleEvent); + } - HttpsFunctionNode(super.firebaseFunctionsNode, this.nativeInstance); + return SchedulerFunctionNode( + functions, + nativeInstance.onSchedule( + options: toNodeScheduleOptions(scheduleOptions), + handler: handleRequest)); + } } -abstract class FirebaseFunctionNode implements FirebaseFunction { - final FirebaseFunctionsNode firebaseFunctionsNode; +class ScheduleEventNode + with SchedulerEventDefaultMixin + implements ScheduleEvent { + final node.JSScheduledEvent nativeInstance; - FirebaseFunctionNode(this.firebaseFunctionsNode); - node.JSFirebaseFunction get nativeInstance; -} + ScheduleEventNode(this.nativeInstance); -node.JSHttpsOptions toNodeHttpsOptions(HttpsOptions httpsOptions) { - return node.JSHttpsOptions( - region: httpsOptions.region, cors: httpsOptions.cors?.toJS); + @override + String? get jobName => nativeInstance.jobName; + + @override + String? get scheduleTime => nativeInstance.scheduleTime; } diff --git a/functions_node/lib/src/node/firebase_functions_node_js_interop.dart b/functions_node/lib/src/node/firebase_functions_node_js_interop.dart index 73e8cad..51b8606 100644 --- a/functions_node/lib/src/node/firebase_functions_node_js_interop.dart +++ b/functions_node/lib/src/node/firebase_functions_node_js_interop.dart @@ -1,12 +1,11 @@ // Copyright (c) 2018, Anatoly Pulyaevskiy. All rights reserved. Use of this source code // is governed by a BSD-style license that can be found in the LICENSE file. -import 'dart:async'; import 'dart:js_interop' as js; import 'dart:js_interop_unsafe'; -import 'package:tekartik_firebase_functions_node/src/import_common.dart'; - +import 'firebase_functions_https_node_js_interop.dart'; +import 'firebase_functions_scheduler_node_js_interop.dart'; import 'import_node.dart'; /// The Firebase Auth service interface. @@ -22,122 +21,10 @@ extension JSFirebaseFonctionsExt on JSFirebaseFonctionsModule { } external JSHttpsFunctions get https; -} - -extension type JSHttpsFunctions._(js.JSObject _) implements js.JSObject {} - -extension JSHttpsFunctionsExt on JSHttpsFunctions { - // Handles HTTPS requests. - // - // Signature: - // export declare function onRequest(opts: HttpsOptions, handler: (request: Request, response: express.Response) => void | Promise): HttpsFunction; - - @js.JS('onRequest') - external JSHttpsFunction _onRequest( - JSHttpsOptions options, _JSHttpsHandler handler); - - @js.JS('onRequest') - external JSHttpsFunction _onRequestNoOptions(_JSHttpsHandler handler); - - JSHttpsFunction onRequest( - {JSHttpsOptions? options, required HttpsHandler handler}) { - js.JSAny? jsHandler(JSHttpsRequest request, JSHttpsResponse response) { - var result = handler(request, response); - if (result is Future) { - return result.toJS; - } else { - return null; - } - } - if (options == null) { - //devPrint('Setting handler no options'); - return _onRequestNoOptions(jsHandler.toJS); - } else { - // devPrint('Setting handler'); - return _onRequest(options, jsHandler.toJS); - } - } + external JSSchedulerFunctions get scheduler; } -// An express request with the wire format representation of the request body. -// -extension type JSHttpsRequest._(js.JSObject _) implements js.JSObject {} - -extension JSHttpsRequestExt on JSHttpsRequest { - /// From node IncomingMessage - /// Request URL string. This contains only the URL that is present in the actual HTTP request. - /// For example: '/status?name=ryan' - external String get url; - - /// Firebase function extension - /// - /// Buffer The wire format representation of the request body. - /// Buffer is not only and extends Uint8Array - external js.JSUint8Array? get rawBody; - - /// The request method as a string. Read only. Examples: 'GET', 'DELETE'. - external String get method; - - /// The request/response headers object. - /// - /// Key-value pairs of header names and values. Header names are lower-cased. - /// - /// Prints something like: - /// - /// { 'user-agent': 'curl/7.22.0', - /// host: '127.0.0.1:8000', - /// accept: '*/*' } - /// console.log(request.headers); COPY - /// Duplicates in raw headers are handled in the following ways, depending on the header name: - /// - /// Duplicates of age, authorization, content-length, content-type, etag, expires, from, host, if-modified-since, if-unmodified-since, last-modified, location, max-forwards, proxy-authorization, referer, retry-after, server, or user-agent are discarded. To allow duplicate values of the headers listed above to be joined, use the option joinDuplicateHeaders in http.request() and http.createServer(). See RFC 9110 Section 5.3 for more information. - /// set-cookie is always an array. Duplicates are added to the array. - /// For duplicate cookie headers, the values are joined together with ; . - /// For all other headers, the values are joined together with , - external js.JSAny? get headers; -} - -extension type JSHttpsResponse._(js.JSObject _) implements js.JSObject { - /// https://expressjs.com/en/4x/api.html#res.send - /// Sends the HTTP response. - /// The body parameter can be a Buffer object, a String, an object, Boolean, or an Array. For example: - external void send([js.JSAny? body]); - - /// Ends the response process. This method actually comes from Node core, - /// specifically the response.end() method of http.ServerResponse. - external void end(); - - /// Sets the HTTP status for the response. - /// It is a chainable alias of Node’s response.statusCode. - external JSHttpsResponse status(int statusCode); - - /// Appends the specified value to the HTTP response header field. - /// If the header is not already set, it creates the header with the - /// specified value. The value parameter can be a string or an array. - /// - /// Note: calling res.set() after res.append() will reset the previously-set header value. - /// - /// res.append('Link', ['', '']) - /// res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly') - /// res.append('Warning', '199 Miscellaneous warning') - @js.JS('append') - external JSHttpsResponse _setHeader(String field, js.JSAny value); - - JSHttpsResponse setHeader(String field, String value) => - _setHeader(field, value.toJS); - JSHttpsResponse setHeaderList(String field, List values) => - _setHeader(field, values.map((e) => e.toJS).toList().toJS); -} - -extension JSHttpsResponseExt on JSHttpsResponse {} - -typedef JSHttpsFunction = js.JSFunction; -typedef _JSHttpsHandler = js.JSFunction; - -typedef HttpsHandler = FutureOr Function( - JSHttpsRequest request, JSHttpsResponse response); - @js.JS('exports') external JSExports get exports; @@ -145,19 +32,15 @@ extension type JSExports._(js.JSObject _) implements js.JSObject {} extension JSExportsExt on JSExports {} -typedef JSFirebaseFunction = js.JSFunction; - -extension type JSHttpsOptions._(js.JSObject _) implements js.JSObject { +/// GlobalOptions are options that can be set across an entire project. +/// These options are common to HTTPS and event handling functions. +extension type JSGlobalOptions._(js.JSObject _) implements js.JSObject { /// Options - external factory JSHttpsOptions( - {String? region, - String? memory, - int? concurrency, - js.JSAny? cors, - int? timeoutSeconds}); + external factory JSGlobalOptions( + {String? region, String? memory, int? concurrency, int? timeoutSeconds}); } -extension JSHttpsOptionsExt on JSHttpsOptions { +extension JSGlobalOptionsExt on JSGlobalOptions { /// String or array string external String? get region; @@ -169,15 +52,6 @@ extension JSHttpsOptionsExt on JSHttpsOptions { /// Number of requests a function can serve at once. external int? get concurrency; - /// string | boolean | RegExp | Array - /// - /// If true, allows CORS on requests to this function. If this is a string or - /// RegExp, allows requests from domains that match the provided value. If - /// this is an Array, allows requests from domains matching at least one entry - /// of the array. Defaults to true for https.CallableFunction and false - /// otherwise. - external js.JSAny? get cors; - /// Timeout for the function in sections, possible values are 0 to 540. HTTPS functions can specify a higher timeout. external int? get timeoutSeconds; } diff --git a/functions_node/lib/src/node/firebase_functions_scheduler_node_js_interop.dart b/functions_node/lib/src/node/firebase_functions_scheduler_node_js_interop.dart new file mode 100644 index 0000000..7d99744 --- /dev/null +++ b/functions_node/lib/src/node/firebase_functions_scheduler_node_js_interop.dart @@ -0,0 +1,88 @@ +// Copyright (c) 2018, Anatoly Pulyaevskiy. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:js_interop' as js; + +import 'package:tekartik_firebase_functions_node/src/import_common.dart'; +import 'package:tekartik_firebase_functions_node/src/node/firebase_functions_node_js_interop.dart'; + +extension type JSSchedulerFunctions._(js.JSObject _) implements js.JSObject {} + +extension JSSchedulerFunctionsExt on JSSchedulerFunctions { + // Handles HTTPS requests. + // + // Signature: + /// onSchedule(options, handler) Handler for scheduled functions. + /// Triggered whenever the associated scheduler job sends a http request. + + @js.JS('onSchedule') + external JSScheduleFunction _onSchedule( + JSScheduleOptions options, _JSScheduleHandler handler); + + JSScheduleFunction onSchedule( + {required JSScheduleOptions options, required ScheduleHandler handler}) { + js.JSAny? jsHandler(JSScheduledEvent data) { + var result = handler(data); + if (result is Future) { + return result.toJS; + } else { + return null; + } + } + + return _onSchedule(options, jsHandler.toJS); + } +} + +typedef JSScheduleFunction = js.JSFunction; +typedef _JSScheduleHandler = js.JSFunction; + +/// Interface representing a ScheduleEvent that is passed to the function handler. +// +// Signature: +// +// +// export interface ScheduledEvent +// Properties +// Property Type Description + +extension type JSScheduledEvent._(js.JSObject _) implements js.JSObject {} + +extension JSScheduledEventExt on JSScheduledEvent { + /// jobName string + /// The Cloud Scheduler job name. Populated via the X-CloudScheduler-JobName header. + /// If invoked manually, this field is undefined. + external String? get jobName; + + /// scheduleTime string + /// For Cloud Scheduler jobs specified in the unix-cron format, + /// this is the job schedule time in RFC3339 UTC "Zulu" format. Populated via the X-CloudScheduler-ScheduleTime header. + /// If the schedule is manually triggered, this field will be the function execution time. + external String? get scheduleTime; +} + +typedef ScheduleHandler = FutureOr Function(JSScheduledEvent event); + +extension type JSScheduleOptions._(js.JSObject _) implements JSGlobalOptions { + /// Options + external factory JSScheduleOptions( + {String? region, + String? memory, + int? concurrency, + required String schedule, + String? timeZone, + int? timeoutSeconds}); +} + +extension JSHttpsOptionsExt on JSScheduleOptions { + /// The schedule, in Unix Crontab or AppEngine syntax. + /// + /// schedule: string + external String? get schedule; + + /// The timezone that the schedule executes in. + /// + /// timeZone?: timezone | Expression | ResetValue + external String? get timeZone; +} diff --git a/functions_node/pubspec.yaml b/functions_node/pubspec.yaml index 89ad82e..9f7cdbe 100644 --- a/functions_node/pubspec.yaml +++ b/functions_node/pubspec.yaml @@ -123,8 +123,10 @@ dependency_overrides: # temp # tekartik_http_node: # path: ../../../tekartik/common_node.dart/http_node -# tekartik_firebase_functions: -# path: ../../../tekartik/firebase_functions.dart/firebase_functions + # tekartik_firebase_functions: + # path: ../../../tekartik/firebase_functions.dart/firebase_functions + # tekartik_firebase_functions_http: + # path: ../../../tekartik/firebase_functions.dart/firebase_functions_http # tekartik_app_node_build: # path: ../../../tekartik/app_node_utils.dart/app_build # Temp