firebasetools

package module
Version: v0.0.16 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 21, 2021 License: MIT Imports: 27 Imported by: 35

README

Build Status Maintained Linting and Tests Coverage Status

Firebase Tools

firebasetools is a Go client library for accessing the Google Firebase and Firestore.

It acts as the entry point to the Firebase Admin SDK. It provides functionality for initializing App instances, which serve as the central entities that provide access to various other Firebase services exposed from the SDK.

Installing it

firebasetools is compatible with modern Go releases in module mode, with Go installed:

go get -u github.com/savannahghi/firebasetools

will resolve and add the package to the current development module, along with its dependencies.

Alternatively the same can be achieved if you use import in a package:

import "github.com/savannahghi/firebasetools"

and run go get without parameters.

The package name is firebasetools

Developing

The default branch library is main

We try to follow semantic versioning ( https://semver.org/ ). For that reason, every major, minor and point release should be tagged.

git tag -m "v0.0.1" "v0.0.1"
git push --tags

Continuous integration tests must pass on Travis CI. Our coverage threshold is 90% i.e you must keep coverage above 90%.

Environment variables

In order to run tests, you need to have an env.sh file similar to this one:

# Application settings
export DEBUG=true
export IS_RUNNING_TESTS=true
export SENTRY_DSN=<a Sentry Data Source Name>

# Google Cloud credentials
export GOOGLE_APPLICATION_CREDENTIALS="<path to a service account JSON file"
export GOOGLE_CLOUD_PROJECT="Google Cloud project id"
export FIREBASE_WEB_API_KEY="<a web API key that corresponds to the project named above>"

# Link shortening
export FIREBASE_DYNAMIC_LINKS_DOMAIN=https://bwlci.page.link
export SERVER_PUBLIC_DOMAIN=https://api-gateway-test.healthcloud.co.ke

# Firestore documents root collection suffix
export ROOT_COLLECTION_SUFFIX="testing"

This file must not be committed to version control.

It is important to export the environment variables. If they are not exported, they will not be visible to child processes e.g go test ./....

These environment variables should also be set up on Travis CI environment variable section.

Contributing

I would like to cover the entire GitHub API and contributions are of course always welcome. The calling pattern is pretty well established, so adding new methods is relatively straightforward. See CONTRIBUTING.md for details.

Versioning

In general, firebasetools follows semver as closely as we can for tagging releases of the package. For self-contained libraries, the application of semantic versioning is relatively straightforward and generally understood. We've adopted the following versioning policy:

  • We increment the major version with any incompatible change to non-preview functionality, including changes to the exported Go API surface or behavior of the API.
  • We increment the minor version with any backwards-compatible changes to functionality, as well as any changes to preview functionality in the GitHub API. GitHub makes no guarantee about the stability of preview functionality, so neither do we consider it a stable part of the go-github API.
  • We increment the patch version with any backwards-compatible bug fixes.

License

This library is distributed under the MIT license found in the LICENSE file.

Documentation

Index

Constants

View Source
const (

	// FirebaseWebAPIKeyEnvVarName is the name of the env var that holds a Firebase web API key
	// for this project
	FirebaseWebAPIKeyEnvVarName = "FIREBASE_WEB_API_KEY"

	// FirebaseCustomTokenSigninURL is the Google Identity Toolkit API for signing in over REST
	FirebaseCustomTokenSigninURL = "https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key="

	// FirebaseRefreshTokenURL is used to request Firebase refresh tokens from Google APIs
	FirebaseRefreshTokenURL = "https://securetoken.googleapis.com/v1/token?key="

	// GoogleApplicationCredentialsEnvVarName is used to obtain service account details from the
	// local server when necessary e.g when running tests on CI or a local developer setup
	GoogleApplicationCredentialsEnvVarName = "GOOGLE_APPLICATION_CREDENTIALS"

	// GoogleProjectNumberEnvVarName is a numeric project number that
	GoogleProjectNumberEnvVarName = "GOOGLE_PROJECT_NUMBER"

	// AuthTokenContextKey is used to add/retrieve the Firebase UID on the context
	AuthTokenContextKey = ContextKey("UID")

	// HTTPClientTimeoutSecs is used to set HTTP client Timeout setting for a request
	HTTPClientTimeoutSecs = 10

	// TestUserEmail is used by integration tests
	TestUserEmail = "test@bewell.co.ke"

	// FDLDomainEnvironmentVariableName is firebase dynamic link domain/URL
	// e.g https://example-one.page.link or https://example-two.page.link
	FDLDomainEnvironmentVariableName = "FIREBASE_DYNAMIC_LINKS_DOMAIN"

	// DefaultPageSize is used to paginate records (e.g those fetched from Firebase)
	// if there is no user specified page size
	DefaultPageSize = 100

	// Sep is a separator, used to create "opaque" IDs
	Sep = "|"

	// DefaultRESTAPIPageSize is the page size to use when calling Slade REST API services if the
	// client does not specify a page size
	DefaultRESTAPIPageSize = 100

	// MaxRestAPIPageSize is the largest page size we'll request
	MaxRestAPIPageSize = 250
)

Variables

View Source
var UnixEpoch = time.Unix(0, 0)

UnixEpoch is used as our version of "time zero". We don't (shouldn't) change it so it's safe to make it a global.

Functions

func AuthenticationMiddleware added in v0.0.13

func AuthenticationMiddleware(firebaseApp IFirebaseApp) func(http.Handler) http.Handler

AuthenticationMiddleware decodes the share session cookie and packs the session into context

func CheckIsAnonymousUser

func CheckIsAnonymousUser(ctx context.Context) (bool, error)

CheckIsAnonymousUser determines if the logged in user is an anonymous user

func CloseRespBody

func CloseRespBody(resp *http.Response)

CloseRespBody closes the body of the supplied HTTP response

func ComposeUnpaginatedQuery

func ComposeUnpaginatedQuery(
	ctx context.Context,
	filter *FilterInput,
	sort *SortInput,
	node Node,
) (*firestore.Query, error)

ComposeUnpaginatedQuery creates a Cloud Firestore query

func CreateAndEncodeCursor added in v0.0.12

func CreateAndEncodeCursor(offset int) *string

CreateAndEncodeCursor creates a cursor and immediately encodes it. It panics if it cannot encode the cursor. These cursors use ZERO BASED indexing.

func CreateFirebaseCustomToken

func CreateFirebaseCustomToken(ctx context.Context, uid string) (string, error)

CreateFirebaseCustomToken creates a custom auth token for the user with the indicated UID

func CreateNode

func CreateNode(ctx context.Context, node Node) (string, time.Time, error)

CreateNode creates a Node on Firebase

func DeleteCollection

func DeleteCollection(
	ctx context.Context,
	client *firestore.Client,
	ref *firestore.CollectionRef,
	batchSize int) error

DeleteCollection deletes a firestore collection

func DeleteNode

func DeleteNode(ctx context.Context, id string, node Node) (bool, error)

DeleteNode retrieves a node from Firestore

func EncodeCursor added in v0.0.12

func EncodeCursor(cursor *Cursor) string

EncodeCursor converts a cursor to a string

func ExtractBearerToken added in v0.0.13

func ExtractBearerToken(r *http.Request) (string, error)

ExtractBearerToken gets a bearer token from an Authorization header.

This is expected to contain a Firebase idToken prefixed with "Bearer "

func ExtractToken added in v0.0.13

func ExtractToken(r *http.Request, header string, prefix string) (string, error)

ExtractToken extracts a token with the specified prefix from the specified header

func GenerateSafeIdentifier

func GenerateSafeIdentifier() string

GenerateSafeIdentifier generates a shortened alphanumeric identifier.

func GetAPIPaginationParams added in v0.0.2

func GetAPIPaginationParams(pagination *PaginationInput) (url.Values, error)

GetAPIPaginationParams composes pagination parameters for use by a REST API that uses offset based pagination

func GetAnonymousContext added in v0.0.6

func GetAnonymousContext(t *testing.T) context.Context

GetAnonymousContext returns an anonymous logged in context, useful for test purposes

func GetAuthToken added in v0.0.6

func GetAuthToken(ctx context.Context, t *testing.T) *auth.Token

GetAuthToken ...

func GetAuthenticatedContext added in v0.0.6

func GetAuthenticatedContext(t *testing.T) context.Context

GetAuthenticatedContext returns a logged in context, useful for test purposes

func GetAuthenticatedContextAndToken added in v0.0.6

func GetAuthenticatedContextAndToken(t *testing.T) (context.Context, *auth.Token)

GetAuthenticatedContextAndToken returns a logged in context and ID token. It is useful for test purposes

func GetAuthenticatedContextFromUID added in v0.0.8

func GetAuthenticatedContextFromUID(ctx context.Context, uid string) (*auth.Token, error)

GetAuthenticatedContextFromUID creates an auth.Token given a valid uid

func GetCollectionName

func GetCollectionName(n Node) string

GetCollectionName calculates the name to give to a node's collection on Firestore

func GetFirebaseAuthClient

func GetFirebaseAuthClient(ctx context.Context) (*auth.Client, error)

GetFirebaseAuthClient initializes a Firebase Authentication client

func GetFirestoreClient

func GetFirestoreClient(ctx context.Context) (*firestore.Client, error)

GetFirestoreClient initializes a Firestore client

func GetFirestoreClientTestUtil added in v0.0.6

func GetFirestoreClientTestUtil(t *testing.T) *firestore.Client

GetFirestoreClientTestUtil ...

func GetFirestoreEnvironmentSuffix

func GetFirestoreEnvironmentSuffix() string

GetFirestoreEnvironmentSuffix get the env suffix where the app is running

func GetLoggedInUserUID added in v0.0.11

func GetLoggedInUserUID(ctx context.Context) (string, error)

GetLoggedInUserUID retrieves the logged in user's Firebase UID from the supplied context and returns an error if it does not succeed

func GetOrCreateAnonymousUser added in v0.0.6

func GetOrCreateAnonymousUser(ctx context.Context) (*auth.UserRecord, error)

GetOrCreateAnonymousUser creates an anonymous user For documentation and test purposes only

func GetOrCreateFirebaseUser

func GetOrCreateFirebaseUser(ctx context.Context, email string) (*auth.UserRecord, error)

GetOrCreateFirebaseUser retrieves the user record of the user with the given email or creates a new one if no user has the specified email

func GetUserTokenFromContext

func GetUserTokenFromContext(ctx context.Context) (*auth.Token, error)

GetUserTokenFromContext retrieves a Firebase *auth.Token from the supplied context

func HasValidFirebaseBearerToken added in v0.0.13

func HasValidFirebaseBearerToken(r *http.Request, firebaseApp IFirebaseApp) (bool, map[string]string, *auth.Token)

HasValidFirebaseBearerToken returns true with no errors if the request has a valid bearer token in the authorization header. Otherwise, it returns false and the error in a map with the key "error"

func NewString

func NewString(s string) *string

NewString returns a pointer to the supplied string.

func OpString

func OpString(op enumutils.Operation) (string, error)

OpString translates between an Operation enum value and the appropriate firestore query operator

func SaveDataToFirestore

func SaveDataToFirestore(firestoreClient *firestore.Client, collection string,
	data interface{}) (string, error)

SaveDataToFirestore takes the supplied data (which can be a map of string to interface{} or a struct with json/firestore tags), a collection name and an intialized firestore client then tries to save the data to that collection.

func ShortenLink(ctx context.Context, longLink string) (string, error)

ShortenLink shortens an FDL link

func SuffixCollection

func SuffixCollection(c string) string

SuffixCollection adds a suffix to the collection name. This will aid in separating collections for different environments

func Typeof

func Typeof(v interface{}) string

Typeof returns the type name for the supplied value

func UpdateNode

func UpdateNode(ctx context.Context, id string, node Node) (time.Time, error)

UpdateNode updates an existing node's document on Firestore

func UpdateRecordOnFirestore

func UpdateRecordOnFirestore(
	firestoreClient *firestore.Client,
	collection string,
	id string,
	data interface{},
) error

UpdateRecordOnFirestore takes the supplied data (which can be a map of string to interface{} or a struct with json/firestore tags), a collection name and an intialized firestore client then tries to update the data in that object

func ValidateBearerToken

func ValidateBearerToken(ctx context.Context, token string) (*auth.Token, error)

ValidateBearerToken checks the bearer token for validity against Firebase

func ValidatePaginationParameters

func ValidatePaginationParameters(pagination *PaginationInput) error

ValidatePaginationParameters ensures that the supplied pagination parameters make sense

Types

type AuditLog

type AuditLog struct {
	ID        uuid.UUID
	RecordID  uuid.UUID        // ID of the audited record
	TypeName  string           // type of the audited record
	Operation string           // e.g pre_save, post_save
	When      time.Time        // timestamp of the operation
	UID       string           // UID of the involved user
	JSON      *json.RawMessage // serialized JSON snapshot
}

AuditLog records changes made to models

type ContextKey

type ContextKey string

ContextKey is used as a type for the UID key for the Firebase *auth.Token on context.Context. It is a custom type in order to minimize context key collissions on the context (.and to shut up golint).

type Cursor added in v0.0.12

type Cursor struct {
	Offset int `json:"offset"`
}

Cursor represents an opaque "position" for a record, for use in pagination

func NewCursor added in v0.0.12

func NewCursor(offset int) *Cursor

NewCursor creates a cursor from an offset and ID

type FilterInput

type FilterInput struct {
	Search   *string        `json:"search"`
	FilterBy []*FilterParam `json:"filterBy"`
}

FilterInput is s generic container for strongly type filter parameters

func (FilterInput) IsEntity

func (f FilterInput) IsEntity()

IsEntity ...

type FilterParam

type FilterParam struct {
	FieldName           string              `json:"fieldName"`
	FieldType           enumutils.FieldType `json:"fieldType"`
	ComparisonOperation enumutils.Operation `json:"comparisonOperation"`
	FieldValue          interface{}         `json:"fieldValue"`
}

FilterParam represents a single field filter parameter

func (FilterParam) IsEntity

func (f FilterParam) IsEntity()

IsEntity ...

type FirebaseAPNSConfigInput added in v0.0.3

type FirebaseAPNSConfigInput struct {
	Headers map[string]interface{} `json:"headers"`
}

FirebaseAPNSConfigInput is used to set Apple APNS settings

type FirebaseAndroidConfigInput added in v0.0.3

type FirebaseAndroidConfigInput struct {
	Priority              string                 `json:"priority"` // one of "normal" or "high"
	CollapseKey           *string                `json:"collapseKey"`
	RestrictedPackageName *string                `json:"restrictedPackageName"`
	Data                  map[string]interface{} `json:"data"` // if specified, overrides the Data field on Message type
}

FirebaseAndroidConfigInput is used to send Firebase Android config values

type FirebaseClient

type FirebaseClient struct{}

FirebaseClient is an implementation of the FirebaseClient interface

func (*FirebaseClient) InitFirebase

func (fc *FirebaseClient) InitFirebase() (IFirebaseApp, error)

InitFirebase ensures that we have a working Firebase configuration

type FirebaseRefreshResponse

type FirebaseRefreshResponse struct {
	ExpiresIn    string `json:"expires_in"`
	TokenType    string `json:"token_type"`
	RefreshToken string `json:"refresh_token"`
	IDToken      string `json:"id_token"`
	UserID       string `json:"user_id"`
	ProjectID    string `json:"project_id"`
}

FirebaseRefreshResponse is used to (de)serialize the results of a successful Firebase token refresh

type FirebaseSimpleNotificationInput added in v0.0.3

type FirebaseSimpleNotificationInput struct {
	Title    string                 `json:"title,omitempty"`
	Body     string                 `json:"body,omitempty"`
	ImageURL *string                `json:"image,omitempty"`
	Data     map[string]interface{} `json:"data,omitempty"`
}

FirebaseSimpleNotificationInput is used to create/send simple FCM notifications

type FirebaseTokenExchangePayload

type FirebaseTokenExchangePayload struct {
	Token             string `json:"token"`
	ReturnSecureToken bool   `json:"returnSecureToken"`
}

FirebaseTokenExchangePayload is marshalled into JSON and sent to the Firebase Auth REST API when exchanging a custom token for an ID token that can be used to make API calls

type FirebaseUserTokens

type FirebaseUserTokens struct {
	IDToken      string `json:"idToken"`
	RefreshToken string `json:"refreshToken"`
	ExpiresIn    string `json:"expiresIn"`
}

FirebaseUserTokens is the unmarshalling target for the JSON response received from the Firebase Auth REST API when exchanging a custom token for an ID token that can be used to make API calls

func AuthenticateCustomFirebaseToken

func AuthenticateCustomFirebaseToken(customAuthToken string) (*FirebaseUserTokens, error)

AuthenticateCustomFirebaseToken takes a custom Firebase auth token and tries to fetch an ID token If successful, a pointer to the ID token is returned Otherwise, an error is returned

type FirebaseWebpushConfigInput added in v0.0.3

type FirebaseWebpushConfigInput struct {
	Headers map[string]interface{} `json:"headers"`
	Data    map[string]interface{} `json:"data"`
}

FirebaseWebpushConfigInput is used to set the Firebase web config

type ID

type ID interface {
	fmt.Stringer
}

ID is fulfilled by all stringifiable types. A valid Relay ID must fulfill this interface.

func MarshalID

func MarshalID(id string, n Node) ID

MarshalID get's a re-fetchable GraphQL Relay ID that combines an objects's ID with it's type and encodes it into an "opaque" Base64 string.

type IDValue

type IDValue string

IDValue represents GraphQL object identifiers

func (IDValue) String

func (val IDValue) String() string

type IFirebaseApp

type IFirebaseApp interface {
	Auth(ctx context.Context) (*auth.Client, error)
	Firestore(ctx context.Context) (*firestore.Client, error)
	Messaging(ctx context.Context) (*messaging.Client, error)
}

IFirebaseApp is an interface that has been extracted in order to support mocking of Firebase auth in tests

type IFirebaseClient

type IFirebaseClient interface {
	InitFirebase() (IFirebaseApp, error)
}

IFirebaseClient defines the Firebase methods that we depend on It has been defined in order to facilitate mocking for tests

type MockFirebaseApp

type MockFirebaseApp struct {
	MockAuthErr    error
	MockAuthClient *auth.Client
	MockRefreshErr error

	MockFirestore    *firestore.Client
	MockFirestoreErr error

	MockMessaging    *messaging.Client
	MockMessagingErr error
}

MockFirebaseApp is used to mock the behavior of a Firebase app for testing

func (*MockFirebaseApp) Auth

func (fa *MockFirebaseApp) Auth(_ context.Context) (*auth.Client, error)

Auth returns a mock client or error, as set on the struct

func (*MockFirebaseApp) Firestore

func (fa *MockFirebaseApp) Firestore(ctx context.Context) (*firestore.Client, error)

Firestore returns a mock Firestore or error, as set on the struct

func (*MockFirebaseApp) Messaging

func (fa *MockFirebaseApp) Messaging(ctx context.Context) (*messaging.Client, error)

Messaging returns a mock Firebase Cloud Messaging client or error, as set on the struct

func (*MockFirebaseApp) RevokeRefreshTokens

func (fa *MockFirebaseApp) RevokeRefreshTokens(ctx context.Context, uid string) error

RevokeRefreshTokens returns an error on attempted refresh

type MockFirebaseClient

type MockFirebaseClient struct {
	MockApp                IFirebaseApp
	MockAppInitErr         error
	MockFirebaseUserTokens *FirebaseUserTokens
	MockFirebaseAuthError  error
}

MockFirebaseClient is used to mock the behavior of a Firebase client for testing

func (*MockFirebaseClient) AuthenticateCustomFirebaseToken

func (fc *MockFirebaseClient) AuthenticateCustomFirebaseToken(_ string, _ *http.Client) (*FirebaseUserTokens, error)

AuthenticateCustomFirebaseToken returns mock user tokens or an error, as set on the struct

func (*MockFirebaseClient) InitFirebase

func (fc *MockFirebaseClient) InitFirebase() (IFirebaseApp, error)

InitFirebase returns a mock Firebase app or error, as set on the struct

type Model

type Model struct {
	ID string `json:"id" firestore:"id"`

	// All models have a non nullable name field
	// If a derived model does not need this, it should use a placeholder e.g "-"
	Name string `json:"name" firestore:"name,omitempty"`

	// All records have an optional description
	Description string `json:"description" firestore:"description,omitempty"`

	// bug alert! If you add "omitempty" to the firestore struct tag, `false`
	// values will not be saved
	Deleted bool `json:"deleted,omitempty" firestore:"deleted"`

	// This is used for audit tracking but is not saved or serialized
	CreatedByUID string `json:"createdByUID" firestore:"createdByUID,omitempty"`
	UpdatedByUID string `json:"updatedByUID" firestore:"updatedByUID,omitempty"`
}

Model defines common behavior for our models. It is also an ideal place to place hooks that are common to all models e.g audit, streaming analytics etc. CAUTION: Model should be evolved with cautions, because of migrations.

func (*Model) GetID

func (c *Model) GetID() ID

GetID returns the struct's ID value

func (Model) IsEntity

func (c Model) IsEntity()

IsEntity ...

func (*Model) IsNode

func (c *Model) IsNode()

IsNode is a "label" that marks this struct (and those that embed it) as implementations of the "Base" interface defined in our GraphQL schema.

func (*Model) SetID

func (c *Model) SetID(id string)

SetID sets the struct's ID value

type Node

type Node interface {
	IsNode()
	GetID() ID
	SetID(string)
}

Node is a Relay (GraphQL Relay) node. Any valid type in this server should be a node.

func RetrieveNode

func RetrieveNode(ctx context.Context, id string, node Node) (Node, error)

RetrieveNode retrieves a node from Firestore

type PageInfo

type PageInfo struct {
	HasNextPage     bool    `json:"hasNextPage"`
	HasPreviousPage bool    `json:"hasPreviousPage"`
	StartCursor     *string `json:"startCursor"`
	EndCursor       *string `json:"endCursor"`
}

PageInfo is used to add pagination information to Relay edges.

func QueryNodes

func QueryNodes(
	ctx context.Context, pagination *PaginationInput,
	filter *FilterInput, sort *SortInput, node Node) ([]*firestore.DocumentSnapshot, *PageInfo, error)

QueryNodes prepares and executes queries against Firebase collections

func (PageInfo) IsEntity

func (p PageInfo) IsEntity()

IsEntity ...

type PaginationInput

type PaginationInput struct {
	First  int    `json:"first"`
	Last   int    `json:"last"`
	After  string `json:"after"`
	Before string `json:"before"`
}

PaginationInput represents paging parameters

func (PaginationInput) IsEntity

func (p PaginationInput) IsEntity()

IsEntity ...

type QueryParam

type QueryParam interface {
	ToURLValues() (values url.Values)
}

QueryParam is an interface used for filter and sort parameters

type SendNotificationPayload added in v0.0.3

type SendNotificationPayload struct {
	RegistrationTokens []string                         `json:"registrationTokens"`
	Data               map[string]string                `json:"data"`
	Notification       *FirebaseSimpleNotificationInput `json:"notification"`
	Android            *FirebaseAndroidConfigInput      `json:"android"`
	Ios                *FirebaseAPNSConfigInput         `json:"ios"`
	Web                *FirebaseWebpushConfigInput      `json:"web"`
}

SendNotificationPayload is used to serialise and save incoming Send FCM notification requests.

type SortInput

type SortInput struct {
	SortBy []*SortParam `json:"sortBy"`
}

SortInput is a generic container for strongly typed sorting parameters

func (SortInput) IsEntity

func (s SortInput) IsEntity()

IsEntity ...

type SortParam

type SortParam struct {
	FieldName string              `json:"fieldName"`
	SortOrder enumutils.SortOrder `json:"sortOrder"`
}

SortParam represents a single field sort parameter

func (SortParam) IsEntity

func (s SortParam) IsEntity()

IsEntity ...

type UserInfo

type UserInfo struct {
	DisplayName string `json:"displayName,omitempty"`
	Email       string `json:"email,omitempty"`
	PhoneNumber string `json:"phoneNumber,omitempty"`
	PhotoURL    string `json:"photoUrl,omitempty"`
	// In the ProviderUserInfo[] ProviderID can be a short domain name (e.g. google.com),
	// or the identity of an OpenID identity provider.
	// In UserRecord.UserInfo it will return the constant string "firebase".
	ProviderID string `json:"providerId,omitempty"`
	UID        string `json:"rawId,omitempty"`
}

UserInfo is a collection of standard profile information for a user.

func GetLoggedInUser

func GetLoggedInUser(ctx context.Context) (*UserInfo, error)

GetLoggedInUser retrieves logged in user information

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
t or T : Toggle theme light dark auto
y or Y : Canonical URL