Skip to content

Commit 06e25b1

Browse files
authored
runtimes/js: Add support for cookies (#1917)
Adds support for adding cookies in auth handler params and in api request/response schema. Generated client will ignore cookies and hand over the responsibility to the browser.
1 parent 2ec73d7 commit 06e25b1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1530
-214
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

e2e-tests/testdata/echo_client/js/client.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export function PreviewEnv(pr) {
2424
return Environment(`pr${pr}`)
2525
}
2626

27+
const BROWSER = typeof globalThis === "object" && ("window" in globalThis);
28+
2729
/**
2830
* Client is an API client for the slug Encore application.
2931
*/
@@ -760,7 +762,7 @@ class BaseClient {
760762

761763
// Add User-Agent header if the script is running in the server
762764
// because browsers do not allow setting User-Agent headers to requests
763-
if (typeof window === "undefined") {
765+
if (!BROWSER) {
764766
this.headers["User-Agent"] = "slug-Generated-JS-Client (Encore/v0.0.0-develop)";
765767
}
766768

e2e-tests/testdata/echo_client/ts/client.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export function PreviewEnv(pr: number | string): BaseURL {
2626
return Environment(`pr${pr}`)
2727
}
2828

29+
const BROWSER = typeof globalThis === "object" && ("window" in globalThis);
30+
2931
/**
3032
* Client is an API client for the slug Encore application.
3133
*/
@@ -169,7 +171,7 @@ export namespace di {
169171
await this.baseClient.callTypedAPI("POST", `/di/one`)
170172
}
171173

172-
public async Three(method: string, body?: BodyInit, options?: CallParameters): Promise<globalThis.Response> {
174+
public async Three(method: string, body?: RequestInit["body"], options?: CallParameters): Promise<globalThis.Response> {
173175
return this.baseClient.callAPI(method, `/di/raw`, body, options)
174176
}
175177

@@ -700,7 +702,7 @@ export namespace test {
700702
* RawEndpoint allows us to test the clients' ability to send raw requests
701703
* under auth
702704
*/
703-
public async RawEndpoint(method: "PUT" | "POST" | "DELETE" | "GET", id: string[], body?: BodyInit, options?: CallParameters): Promise<globalThis.Response> {
705+
public async RawEndpoint(method: "PUT" | "POST" | "DELETE" | "GET", id: string[], body?: RequestInit["body"], options?: CallParameters): Promise<globalThis.Response> {
704706
return this.baseClient.callAPI(method, `/raw/blah/${id.map(encodeURIComponent).join("/")}`, body, options)
705707
}
706708

@@ -1026,7 +1028,7 @@ class BaseClient {
10261028

10271029
// Add User-Agent header if the script is running in the server
10281030
// because browsers do not allow setting User-Agent headers to requests
1029-
if ( typeof globalThis === "object" && !("window" in globalThis) ) {
1031+
if (!BROWSER) {
10301032
this.headers["User-Agent"] = "slug-Generated-TS-Client (Encore/v0.0.0-develop)";
10311033
}
10321034

@@ -1146,15 +1148,15 @@ class BaseClient {
11461148
}
11471149

11481150
// callTypedAPI makes an API call, defaulting content type to "application/json"
1149-
public async callTypedAPI(method: string, path: string, body?: BodyInit, params?: CallParameters): Promise<Response> {
1151+
public async callTypedAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise<Response> {
11501152
return this.callAPI(method, path, body, {
11511153
...params,
11521154
headers: { "Content-Type": "application/json", ...params?.headers }
11531155
});
11541156
}
11551157

11561158
// callAPI is used by each generated API method to actually make the request
1157-
public async callAPI(method: string, path: string, body?: BodyInit, params?: CallParameters): Promise<Response> {
1159+
public async callAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise<Response> {
11581160
let { query, headers, ...rest } = params ?? {}
11591161
const init = {
11601162
...this.requestInit,

parser/encoding/rpc.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,14 @@ var requestTags = map[string]tagDescription{
6161
"query": QueryTag,
6262
"qs": QsTag,
6363
"header": HeaderTag,
64+
"cookie": CookieTag,
6465
"json": JSONTag,
6566
}
6667

6768
// responseTags is a description of tags used for responses
6869
var responseTags = map[string]tagDescription{
6970
"header": HeaderTag,
71+
"cookie": CookieTag,
7072
"json": JSONTag,
7173
}
7274

@@ -154,18 +156,19 @@ func (e *AuthEncoding) ParameterEncodingMapByName() map[string][]*ParameterEncod
154156
type ResponseEncoding struct {
155157
// Contains metadata about how to marshal an HTTP parameter
156158
HeaderParameters []*ParameterEncoding `json:"header_parameters"`
159+
CookieParameters []*ParameterEncoding `json:"cookie_parameters"`
157160
BodyParameters []*ParameterEncoding `json:"body_parameters"`
158161
}
159162

160163
// ParameterEncodingMap returns the parameter encodings as a map, keyed by SrcName.
161164
func (e *ResponseEncoding) ParameterEncodingMap() map[string]*ParameterEncoding {
162-
return toEncodingMap(srcNameKey, e.HeaderParameters, e.BodyParameters)
165+
return toEncodingMap(srcNameKey, e.HeaderParameters, e.CookieParameters, e.BodyParameters)
163166
}
164167

165168
// ParameterEncodingMapByName returns the parameter encodings as a map, keyed by Name.
166169
// Conflicts result in an undefined encoding getting set.
167170
func (e *ResponseEncoding) ParameterEncodingMapByName() map[string][]*ParameterEncoding {
168-
return toEncodingMultiMap(nameKey, e.HeaderParameters, e.BodyParameters)
171+
return toEncodingMultiMap(nameKey, e.HeaderParameters, e.CookieParameters, e.BodyParameters)
169172
}
170173

171174
// RequestEncoding expresses how a request should be encoded for an explicit set of HTTPMethods
@@ -175,18 +178,19 @@ type RequestEncoding struct {
175178
// Contains metadata about how to marshal an HTTP parameter
176179
HeaderParameters []*ParameterEncoding `json:"header_parameters"`
177180
QueryParameters []*ParameterEncoding `json:"query_parameters"`
181+
CookieParameters []*ParameterEncoding `json:"cookie_parameters"`
178182
BodyParameters []*ParameterEncoding `json:"body_parameters"`
179183
}
180184

181185
// ParameterEncodingMap returns the parameter encodings as a map, keyed by SrcName.
182186
func (e *RequestEncoding) ParameterEncodingMap() map[string]*ParameterEncoding {
183-
return toEncodingMap(srcNameKey, e.HeaderParameters, e.QueryParameters, e.BodyParameters)
187+
return toEncodingMap(srcNameKey, e.HeaderParameters, e.QueryParameters, e.BodyParameters, e.CookieParameters)
184188
}
185189

186190
// ParameterEncodingMapByName returns the parameter encodings as a map, keyed by Name.
187191
// Conflicts result in an undefined encoding getting set.
188192
func (e *RequestEncoding) ParameterEncodingMapByName() map[string][]*ParameterEncoding {
189-
return toEncodingMultiMap(nameKey, e.HeaderParameters, e.QueryParameters, e.BodyParameters)
193+
return toEncodingMultiMap(nameKey, e.HeaderParameters, e.QueryParameters, e.BodyParameters, e.CookieParameters)
190194
}
191195

192196
// ParameterEncoding expresses how a parameter should be encoded on the wire
@@ -302,6 +306,7 @@ func DescribeRPC(appMetaData *meta.Data, rpc *meta.RPC, options *Options) (*RPCE
302306
HeaderParameters: defaultEncoding.HeaderParameters,
303307
BodyParameters: defaultEncoding.BodyParameters,
304308
QueryParameters: defaultEncoding.QueryParameters,
309+
CookieParameters: defaultEncoding.CookieParameters,
305310
}
306311
}
307312

@@ -565,12 +570,13 @@ func DescribeResponse(appMetaData *meta.Data, responseSchema *schema.Type, optio
565570
if err != nil {
566571
return nil, err
567572
}
568-
if keys := keyDiff(fields, Header, Body); len(keys) > 0 {
569-
return nil, errors.Newf("response must only contain body and header parameters. Found: %v", keys)
573+
if keys := keyDiff(fields, Header, Body, Cookie); len(keys) > 0 {
574+
return nil, errors.Newf("response must only contain body, header and cookie parameters. Found: %v", keys)
570575
}
571576
return &ResponseEncoding{
572577
BodyParameters: fields[Body],
573578
HeaderParameters: fields[Header],
579+
CookieParameters: fields[Cookie],
574580
}, nil
575581
}
576582

@@ -620,13 +626,14 @@ func DescribeRequest(appMetaData *meta.Data, requestSchema *schema.Type, options
620626
}
621627
}
622628

623-
if keys := keyDiff(fields, Query, Header, Body); len(keys) > 0 {
624-
return nil, errors.Newf("request must only contain Query, Body and Header parameters. Found: %v", keys)
629+
if keys := keyDiff(fields, Query, Header, Body, Cookie); len(keys) > 0 {
630+
return nil, errors.Newf("request must only contain Query, Body, Header and Cookie parameters. Found: %v", keys)
625631
}
626632
reqs = append(reqs, &RequestEncoding{
627633
HTTPMethods: methods,
628634
QueryParameters: fields[Query],
629635
HeaderParameters: fields[Header],
636+
CookieParameters: fields[Cookie],
630637
BodyParameters: fields[Body],
631638
})
632639
}

pkg/clientgen/javascript.go

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,10 @@ func (js *javascript) Generate(p clientgentypes.GenerateParams) (err error) {
7777
js.typs = getNamedTypes(p.Meta, p.Services)
7878

7979
if js.md.AuthHandler != nil {
80-
js.hasAuth = true
81-
js.authIsComplexType = js.md.AuthHandler.Params.GetBuiltin() != schema.Builtin_STRING
80+
if !js.isAuthCookiesOnly() {
81+
js.hasAuth = true
82+
js.authIsComplexType = js.md.AuthHandler.Params.GetBuiltin() != schema.Builtin_STRING
83+
}
8284
}
8385

8486
js.WriteString("// " + doNotEditHeader() + "\n\n")
@@ -106,6 +108,41 @@ func (js *javascript) Generate(p clientgentypes.GenerateParams) (err error) {
106108
return nil
107109
}
108110

111+
func (js *javascript) getFields(typ *schema.Type) []*schema.Field {
112+
if typ == nil {
113+
return nil
114+
}
115+
switch typ.Typ.(type) {
116+
case *schema.Type_Struct:
117+
return typ.GetStruct().Fields
118+
case *schema.Type_Named:
119+
decl := js.md.Decls[typ.GetNamed().Id]
120+
return js.getFields(decl.Type)
121+
default:
122+
return nil
123+
}
124+
}
125+
126+
func (js *javascript) isEmptyObject(typ *schema.Type) bool {
127+
fields := js.getFields(typ)
128+
if fields == nil {
129+
return false
130+
}
131+
for _, field := range fields {
132+
if field.Wire.GetCookie() == nil {
133+
return false
134+
}
135+
}
136+
return true
137+
}
138+
139+
func (js *javascript) isAuthCookiesOnly() bool {
140+
if js.md.AuthHandler == nil {
141+
return false
142+
}
143+
return js.isEmptyObject(js.md.AuthHandler.Params)
144+
}
145+
109146
func (js *javascript) writeService(svc *meta.Service, set clientgentypes.ServiceSet, tags clientgentypes.TagSet) error {
110147
// Determine if we have anything worth exposing.
111148
// Either a public RPC or a named type.
@@ -199,7 +236,8 @@ func (js *javascript) writeService(svc *meta.Service, set clientgentypes.Service
199236
// Avoid a name collision.
200237
payloadName := "params"
201238

202-
if (!isStream && rpc.RequestSchema != nil) || (isStream && rpc.HandshakeSchema != nil) {
239+
if (!isStream && (rpc.RequestSchema != nil && !js.isEmptyObject(rpc.RequestSchema))) ||
240+
(isStream && (rpc.HandshakeSchema != nil && !js.isEmptyObject(rpc.HandshakeSchema))) {
203241
if nParams > 0 {
204242
js.WriteString(", ")
205243
}
@@ -470,7 +508,7 @@ func (js *javascript) rpcCallSite(w *indentWriter, rpc *meta.RPC, rpcPath string
470508
callAPI += ")"
471509

472510
// If there's no response schema, we can just return the call to the API directly
473-
if rpc.ResponseSchema == nil {
511+
if rpc.ResponseSchema == nil || js.isEmptyObject(rpc.ResponseSchema) {
474512
w.WriteStringf("await %s\n", callAPI)
475513
return nil
476514
}
@@ -489,12 +527,23 @@ func (js *javascript) rpcCallSite(w *indentWriter, rpc *meta.RPC, rpcPath string
489527
w.WriteString("\n//Populate the return object from the JSON body and received headers\nconst rtn = await resp.json()\n")
490528

491529
for _, headerField := range respEnc.HeaderParameters {
530+
isSetCookie := strings.ToLower(headerField.WireFormat) == "set-cookie"
531+
if isSetCookie {
532+
w.WriteString("// Skip set-cookie header in browser context as browsers doesn't have access to read it\n")
533+
w.WriteString("if (!BROWSER) {\n")
534+
w = w.Indent()
535+
}
536+
492537
js.seenHeaderResponse = true
493538
fieldValue := fmt.Sprintf("mustBeSet(\"Header `%s`\", resp.headers.get(\"%s\"))", headerField.WireFormat, headerField.WireFormat)
494539

495540
w.WriteStringf("%s = %s\n", js.Dot("rtn", headerField.SrcName), js.convertStringToBuiltin(headerField.Type.GetBuiltin(), fieldValue))
496-
}
497541

542+
if isSetCookie {
543+
w = w.Dedent()
544+
w.WriteString("}\n")
545+
}
546+
}
498547
w.WriteString("return rtn\n")
499548
return nil
500549
}
@@ -544,6 +593,8 @@ export function PreviewEnv(pr) {
544593
return Environment(` + "`pr${pr}`" + `)
545594
}
546595
596+
const BROWSER = typeof globalThis === "object" && ("window" in globalThis);
597+
547598
/**
548599
* Client is an API client for the ` + js.appSlug + ` Encore application.
549600
*/
@@ -751,7 +802,7 @@ class BaseClient {`)
751802
752803
// Add User-Agent header if the script is running in the server
753804
// because browsers do not allow setting User-Agent headers to requests
754-
if (typeof window === "undefined") {
805+
if (!BROWSER) {
755806
this.headers["User-Agent"] = "` + userAgent + `";
756807
}
757808

pkg/clientgen/testdata/goapp/expected_baseauth_javascript.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export function PreviewEnv(pr) {
2424
return Environment(`pr${pr}`)
2525
}
2626

27+
const BROWSER = typeof globalThis === "object" && ("window" in globalThis);
28+
2729
/**
2830
* Client is an API client for the app Encore application.
2931
*/
@@ -270,7 +272,7 @@ class BaseClient {
270272

271273
// Add User-Agent header if the script is running in the server
272274
// because browsers do not allow setting User-Agent headers to requests
273-
if (typeof window === "undefined") {
275+
if (!BROWSER) {
274276
this.headers["User-Agent"] = "app-Generated-JS-Client (Encore/v0.0.0-develop)";
275277
}
276278

pkg/clientgen/testdata/goapp/expected_baseauth_typescript.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export function PreviewEnv(pr: number | string): BaseURL {
2626
return Environment(`pr${pr}`)
2727
}
2828

29+
const BROWSER = typeof globalThis === "object" && ("window" in globalThis);
30+
2931
/**
3032
* Client is an API client for the app Encore application.
3133
*/
@@ -359,7 +361,7 @@ class BaseClient {
359361

360362
// Add User-Agent header if the script is running in the server
361363
// because browsers do not allow setting User-Agent headers to requests
362-
if ( typeof globalThis === "object" && !("window" in globalThis) ) {
364+
if (!BROWSER) {
363365
this.headers["User-Agent"] = "app-Generated-TS-Client (Encore/v0.0.0-develop)";
364366
}
365367

@@ -472,15 +474,15 @@ class BaseClient {
472474
}
473475

474476
// callTypedAPI makes an API call, defaulting content type to "application/json"
475-
public async callTypedAPI(method: string, path: string, body?: BodyInit, params?: CallParameters): Promise<Response> {
477+
public async callTypedAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise<Response> {
476478
return this.callAPI(method, path, body, {
477479
...params,
478480
headers: { "Content-Type": "application/json", ...params?.headers }
479481
});
480482
}
481483

482484
// callAPI is used by each generated API method to actually make the request
483-
public async callAPI(method: string, path: string, body?: BodyInit, params?: CallParameters): Promise<Response> {
485+
public async callAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise<Response> {
484486
let { query, headers, ...rest } = params ?? {}
485487
const init = {
486488
...this.requestInit,

pkg/clientgen/testdata/goapp/expected_javascript.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export function PreviewEnv(pr) {
2424
return Environment(`pr${pr}`)
2525
}
2626

27+
const BROWSER = typeof globalThis === "object" && ("window" in globalThis);
28+
2729
/**
2830
* Client is an API client for the app Encore application.
2931
*/
@@ -466,7 +468,7 @@ class BaseClient {
466468

467469
// Add User-Agent header if the script is running in the server
468470
// because browsers do not allow setting User-Agent headers to requests
469-
if (typeof window === "undefined") {
471+
if (!BROWSER) {
470472
this.headers["User-Agent"] = "app-Generated-JS-Client (Encore/v0.0.0-develop)";
471473
}
472474

0 commit comments

Comments
 (0)