parse

package module
v2.3.1 Latest Latest
Warning

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

Go to latest
Published: Jan 29, 2017 License: BSD-3-Clause Imports: 27 Imported by: 0

README

#Parse

Godoc license

This package provides a client for Parse's REST API. So far, it supports most of the query operations provided by Parse's Javascript library, with a few exceptions (listed below under TODO).

###Installation

go get github.com/kylemcc/parse

###Documentation Full documentation is provided by godoc.org

###Usage:

package main

import (
    "fmt"
	"time"
    
    "github.com/kylemcc/parse"
)

func main() {
    parse.Initialize("APP_ID", "REST_KEY", "MASTER_KEY") // master key is optional
    
    user := parse.User{}
    q, err := parse.NewQuery(&user)
	if err != nil {
		panic(err)
	}
    q.EqualTo("email", "kylemcc@gmail.com")
    q.GreaterThan("numFollowers", 10).OrderBy("-createdAt") // API is chainable
    err := q.First()
    if err != nil {
        if pe, ok := err.(parse.ParseError); ok {
            fmt.Printf("Error querying parse: %d - %s\n", pe.Code(), pe.Message())
        }
    }
    
    fmt.Printf("Retrieved user with id: %s\n", u.Id)

	q2, _ := parse.NewQuery(&parse.User{})
	q2.GreaterThan("createdAt", time.Date(2014, 01, 01, 0, 0, 0, 0, time.UTC))

	rc := make(chan *parse.User)

	// .Each will retrieve all results for a query and send them to the provided channel
	// The iterator returned allows for early cancelation of the iteration process, and
	// stores any error that triggers early termination
	iterator, err := q2.Each(rc)
	for u := range rc{
		fmt.Printf("received user: %v\n", u)
		// Do something
		if err := process(u); err != nil {
			// Cancel if there was an error
			iterator.Cancel()
		}
	}

	// An error occurred - not all rows were processed
	if it.Error() != nil {
		panic(it.Error())
	}
}

###TODO

  • Missing query operations
    • Related to
  • Missing CRUD operations:
    • Update
      • Field ops (__op):
        • AddRelation
        • RemoveRelation
  • Roles
  • Cloud Functions
  • Background Jobs
  • Analytics
  • File upload/retrieval
  • Batch operations

Documentation

Overview

Package parse provides a full-featured client for the Parse (http://parse.com) PAAS REST API

Index

Constants

View Source
const (
	ParseVersion       = "1"
	AppIdHeader        = "X-Parse-Application-Id"
	RestKeyHeader      = "X-Parse-REST-API-Key"
	MasterKeyHeader    = "X-Parse-Master-Key"
	SessionTokenHeader = "X-Parse-Session-Token"
	UserAgentHeader    = "User-Agent"
)

Variables

View Source
var ErrNoRows = errors.New("no results returned")

Returned when a query returns no results

View Source
var MetricsRegistry = metrics.NewRegistry()

MetricsRegistry is so you can get at the metrics yourself

Functions

func CallFunction

func CallFunction(name string, params Params, resp interface{}) error

func Create

func Create(v interface{}, useMasterKey bool) error

Save a new instance of the type pointed to by v to the Parse database. If useMasteKey=true, the Master Key will be used for the creation request. On a successful request, the CreatedAt field will be set on v.

Note: v should be a pointer to a struct whose name represents a Parse class, or that implements the ClassName method

func Delete

func Delete(v interface{}, useMasterKey bool) error

Delete the instance of the type represented by v from the Parse database. If useMasteKey=true, the Master Key will be used for the deletion request.

func Initialize

func Initialize(appID, restKey, masterKey string, host string, scheme string, mountPoint string)

Initialize the parse library with your API keys

func LinkFacebookAccount

func LinkFacebookAccount(u *User, a *FacebookAuthData) error

func RegisterType

func RegisterType(t interface{}) error

Register a type so that it can be handled when populating struct values.

The provided value will be registered under the name provided by the ClassName method if it is implemented, otherwise by the name of the type. When handling Parse responses, any object value with __type "Object" or "Pointer" and className matching the type provided will be unmarshaled into pointer to the provided type.

This is useful in at least one instance: If you have an array or object field on a Parse class that contains pointers to or instances of Objects of arbitrary types that cannot be represented by a single type on your struct definition, but you would still like to be able to populate your struct with these values.

In order to accomplish this, the field in question on your struct definition should either be of type interface{}, or another interface type that all possible types implement.

Accepts a value t, representing the type to be registered. The value t should be either a struct value, or a pointer to a struct. Otherwise, an error will be returned.

func SetHTTPClient

func SetHTTPClient(c *http.Client) error

SetHTTPClient provide an httpClient for the rest calls

func SetHTTPTimeout

func SetHTTPTimeout(t time.Duration) error

SetHTTPTimeout Set the timeout for requests to Parse

Returns an error if called before parse.Initialize

func SetRateLimit

func SetRateLimit(limit, burst uint) error

SetRateLimit Set the maximum number of requests per second, with an optional burst rate.

Returns an error if called before parse.Initialize

If this option is set, this library will restrict calling code to a maximum number of requests per second. Requests exceeding this limit will block for the appropriate period of time.

func SetUserAgent

func SetUserAgent(ua string) error

SetUserAgent Set the User Agent to be specified for requests against Parse

Returns an error if called before parse.Initialize

func SetupMetricExternalLogging added in v2.3.1

func SetupMetricExternalLogging(cloudwatchAccessKey string, cloudwatchAccessSecret string, namespace string)

SetupMetricExternalLogging will setup sending the counter to cloudwatch

func SetupMetricFileLogging added in v2.3.1

func SetupMetricFileLogging(metricFilePath string)

SetupMetricFileLogging will setup sending the counter to a file

func Signup

func Signup(username string, password string, user interface{}) error

Types

type ACL

type ACL interface {
	// Returns whether public read access is enabled on this ACL
	PublicReadAccess() bool

	// Returns whether public write access is enabled on this ACL
	PublicWriteAccess() bool

	// Returns whether read access is enabled on this ACL for the
	// given role
	RoleReadAccess(role string) bool

	// Returns whether write access is enabled on this ACL for the
	// given role
	RoleWriteAccess(role string) bool

	// Returns whether read access is enabled on this ACL for the
	// given user
	ReadAccess(userId string) bool

	// Returns whether write access is enabled on this ACL for the
	// given user
	WriteAccess(userId string) bool

	// Allow the object to which this ACL is attached be read
	// by anyone
	SetPublicReadAccess(allowed bool) ACL

	// Allow the object to which this ACL is attached to be
	// updated by anyone
	SetPublicWriteAccess(allowed bool) ACL

	// Allow the object to which this ACL is attached to be
	// read by the provided role
	SetRoleReadAccess(role string, allowed bool) ACL

	// Allow the object to which this ACL is attached to be
	// updated by the provided role
	SetRoleWriteAccess(role string, allowed bool) ACL

	// Allow the object to which this ACL is attached to be
	// read by the provided user
	SetReadAccess(userId string, allowed bool) ACL

	// Allow the object to which this ACL is attached to be
	// updated by the provided user
	SetWriteAccess(userId string, allowed bool) ACL
}

func NewACL

func NewACL() ACL

type AnonymousAuthData

type AnonymousAuthData struct {
	Id string `json:"id"`
}

type AuthData

type AuthData struct {
	Twitter   *TwitterAuthData   `json:"twitter,omitempty"`
	Facebook  *FacebookAuthData  `json:"facebook,omitempty"`
	Anonymous *AnonymousAuthData `json:"anonymous,omitempty"`
}

type Base

type Base struct {
	Id        string                 `parse:"objectId"`
	CreatedAt time.Time              `parse:"-"`
	UpdatedAt time.Time              `parse:"-"`
	ACL       ACL                    `parse:"ACL,omitempty"`
	Extra     map[string]interface{} `parse:"-"`
}

A base type containing fields common to all Parse types

Embed this struct in custom types to avoid having to declare these fields everywhere.

type Config

type Config map[string]interface{}

func GetConfig

func GetConfig() (Config, error)

func (Config) Bool

func (c Config) Bool(key string) bool

Retrieves the value associated with the given key, and, if present, converts the value to a bool and returns it. If the value is not present, or is not a bool value, false is returned

func (Config) Bytes

func (c Config) Bytes(key string) []byte

Retrieves the value associated with the given key, and, if present, converts the value to a byte slice and returns it. If the value is not present, or is not a string value, an empty byte slice is returned

func (Config) Float

func (c Config) Float(key string) float64

Retrieves the value associated with the given key, and, if present, converts the value to an float64 and returns it. If the value is not present, or is not a numeric value, 0 is returned

func (Config) Floats

func (c Config) Floats(key string) []float64

Retrieves the value associated with the given key, and, if present, converts the value to a slice of float64 values and returns it. If the value is not present, or is not an array value, nil is returned

func (Config) Int

func (c Config) Int(key string) int

Retrieves the value associated with the given key, and, if present, converts the value to an int and returns it. If the value is not present, or is not a numeric value, 0 is returned

func (Config) Int64

func (c Config) Int64(key string) int64

Retrieves the value associated with the given key, and, if present, converts the value to an int64 and returns it. If the value is not present, or is not a numeric value, 0 is returned

func (Config) Int64s

func (c Config) Int64s(key string) []int64

Retrieves the value associated with the given key, and, if present, converts the value to a slice of int64 values and returns it. If the value is not present, or is not an array value, nil is returned

func (Config) Ints

func (c Config) Ints(key string) []int

Retrieves the value associated with the given key, and, if present, converts the value to a slice of int values and returns it. If the value is not present, or is not an array value, nil is returned

func (Config) Map

func (c Config) Map(key string) Config

Retrieves the value associated with the given key, and, if present, converts value to a Config type (map[string]interface{}) and returns it. If the value is not present, or is not a JSON object, nil is returned

func (Config) String

func (c Config) String(key string) string

Retrieves the value associated with the given key, and, if present, converts the value to a string and returns it. If the value is not present, or is not a string value, an empty string is returned

func (Config) Strings

func (c Config) Strings(key string) []string

Retrieves the value associated with the given key, and, if present, converts the value to a slice of string values and returns it. If the value is not present, or is not an array value, nil is returned

func (Config) Values

func (c Config) Values(key string) []interface{}

Retrieves the value associated with the given key, and, if present, converts the value to a slice of interface{} values and returns it. If the value is not present, or is not an array value, nil is returned

type Date

type Date time.Time

Represents the Parse Date type. Values of type time.Time will automatically converted to a Date type when constructing queries or creating objects. The inverse is true for retrieving objects. Direct use of this type should not be necessary

func (Date) MarshalJSON

func (d Date) MarshalJSON() ([]byte, error)

func (*Date) UnmarshalJSON

func (d *Date) UnmarshalJSON(b []byte) error

type Error added in v2.3.1

type Error interface {
	error
	Code() int
	Message() string
	StatusCode() int
	RequestMethod() string
	RequestURL() string
	RequestHeaders() []string
	RequestBody() string
	ResponseBody() string
}

Error is the all the methods you can call on a parse error

type FacebookAuthData

type FacebookAuthData struct {
	Id             string
	AccessToken    string    `parse:"access_token"`
	ExpirationDate time.Time `parse:"expiration_date"`
}

func (*FacebookAuthData) MarshalJSON

func (a *FacebookAuthData) MarshalJSON() ([]byte, error)

func (*FacebookAuthData) UnmarshalJSON

func (a *FacebookAuthData) UnmarshalJSON(b []byte) (err error)

type File

type File struct {
	Name string `json:"name"`
	Url  string `json:"url"`
}

Represents the Parse File type

func (*File) MarshalJSON

func (f *File) MarshalJSON() ([]byte, error)

type GeoPoint

type GeoPoint struct {
	Latitude  float64
	Longitude float64
}

Represents the Parse GeoPoint type

func (GeoPoint) KilometersTo

func (g GeoPoint) KilometersTo(point GeoPoint) float64

Returns this distance from this GeoPoint to another in kilometers

func (GeoPoint) MarshalJSON

func (g GeoPoint) MarshalJSON() ([]byte, error)

func (GeoPoint) MilesTo

func (g GeoPoint) MilesTo(point GeoPoint) float64

Returns this distance from this GeoPoint to another in miles

func (GeoPoint) RadiansTo

func (g GeoPoint) RadiansTo(point GeoPoint) float64

Returns this distance from this GeoPoint to another in radians

func (*GeoPoint) UnmarshalJSON

func (g *GeoPoint) UnmarshalJSON(b []byte) error

type Installation

type Installation struct {
	Base
	Badge          int      `parse:",omitempty"`
	Channels       []string `parse:",omitempty"`
	TimeZone       string
	DeviceType     string
	PushType       string `parse:",omitempty"`
	GCMSenderId    string `parse:",omitempty"`
	InstallationId string
	DeviceToken    string   `parse:",omitempty"`
	ChannelUris    []string `parse:",omitempty"`
	AppName        string
	AppVersion     string
	ParseVersion   string
	AppIdentifier  string
}

Represents the built-in Parse "Installation" class. Embed this type in a custom type containing any custom fields. When fetching user objects, any retrieved fields with no matching struct field will be stored in User.Extra (map[string]interface{})

func (*Installation) ClassName

func (i *Installation) ClassName() string

func (*Installation) Endpoint

func (i *Installation) Endpoint() string

type Iterator

type Iterator struct {
	// contains filtered or unexported fields
}

func (*Iterator) Cancel

func (i *Iterator) Cancel()

Cancel interating over the current query. This is a no-op if iteration has already terminated

func (*Iterator) CancelError

func (i *Iterator) CancelError(err error)

Cancel iterating over the current query, and set the iterator's error value to the provided error.

func (*Iterator) Done

func (i *Iterator) Done() <-chan error

Returns a channel that is closed once iteration is finished. Any error causing iteration to terminate prematurely will be available on this channel.

func (*Iterator) Error

func (i *Iterator) Error() error

Returns the terminal error value of the iteration process, or nil if the iteration process exited normally (or hasn't started yet)

type Params

type Params map[string]interface{}

type Pointer

type Pointer struct {
	Id        string
	ClassName string
}

Represents a Parse Pointer type. When querying, creating, or updating objects, any struct types will be automatically converted to and from Pointer types as required. Direct use of this type should not be necessary

func (Pointer) MarshalJSON

func (p Pointer) MarshalJSON() ([]byte, error)

type PushNotification

type PushNotification interface {
	// Set the query for advanced targeting
	//
	// use parse.NewPushQuery to create a new query
	Where(q Query) PushNotification

	// Set the channels to target
	Channels(c ...string) PushNotification

	// Specify a specific time to send this push
	PushTime(t time.Time) PushNotification

	// Set the time this push notification should expire if it can't be immediately sent
	ExpirationTime(t time.Time) PushNotification

	// Set the duration after which this push notification should expire if it can't be immediately sent
	ExpirationInterval(d time.Duration) PushNotification

	// Set the payload for this push notification
	Data(d map[string]interface{}) PushNotification

	// Send the push notification
	Send() error
}

Interface representing a Parse Push notification and the various options for sending a push notification. This API is chainable for conveniently building push notifications:

parse.NewPushNotification().Channels("chan1", "chan2").Where(parse.NewPushQuery().EqualTo("deviceType", "ios")).Data(map[string]interface{}{"alert": "hello"}).Send()

func NewPushNotification

func NewPushNotification() PushNotification

Create a new Push Notifaction

See the Push Notification Guide for more details: https://www.parse.com/docs/push_guide#sending/REST

type Query

type Query interface {

	// Use the Master Key for the given request.
	UseMasterKey() Query

	// Get retrieves the instance of the type pointed to by v and
	// identified by id, and stores the result in v.
	Get(id string) error

	// Set the sort order for the query. The first argument sets the primary
	// sort order. Subsequent arguments will set secondary sort orders. Results
	// will be sorted in ascending order by default. Prefix field names with a
	// '-' to sort in descending order. E.g.: q.OrderBy("-createdAt") will sort
	// by the createdAt field in descending order.
	OrderBy(fs ...string) Query

	// Set the number of results to retrieve
	Limit(l int) Query

	// Set the number of results to skip before returning any results
	Skip(s int) Query

	// Specify nested fields to retrieve within the primary object. Use
	// dot notation to retrieve further nested fields. E.g.:
	// q.Include("user") or q.Include("user.location")
	Include(fs ...string) Query

	// Only retrieve the specified fields
	Keys(fs ...string) Query

	// Add a constraint requiring the field specified by f be equal to the
	// value represented by v
	EqualTo(f string, v interface{}) Query

	// Add a constraint requiring the field specified by f not be equal to the
	// value represented by v
	NotEqualTo(f string, v interface{}) Query

	// Add a constraint requiring the field specified by f be greater than the
	// value represented by v
	GreaterThan(f string, v interface{}) Query

	// Add a constraint requiring the field specified by f be greater than or
	// or equal to the value represented by v
	GreaterThanOrEqual(f string, v interface{}) Query

	// Add a constraint requiring the field specified by f be less than the
	// value represented by v
	LessThan(f string, v interface{}) Query

	// Add a constraint requiring the field specified by f be less than or
	// or equal to the value represented by v
	LessThanOrEqual(f string, v interface{}) Query

	// Add a constraint requiring the field specified by f be equal to one
	// of the values specified
	In(f string, vs ...interface{}) Query

	// Add a constraint requiring the field specified by f not be equal to any
	// of the values specified
	NotIn(f string, vs ...interface{}) Query

	// Add a constraint requiring returned objects contain the field specified by f
	Exists(f string) Query

	// Add a constraint requiring returned objects do not contain the field specified by f
	DoesNotExist(f string) Query

	// Add a constraint requiring the field specified by f contain all
	// of the values specified
	All(f string, vs ...interface{}) Query

	// Add a constraint requiring the string field specified by f contain
	// the substring specified by v
	Contains(f string, v string) Query

	// Add a constraint requiring the string field specified by f start with
	// the substring specified by v
	StartsWith(f string, v string) Query

	// Add a constraint requiring the string field specified by f end with
	// the substring specified by v
	EndsWith(f string, v string) Query

	// Add a constraint requiring the string field specified by f match the
	// regular expression v
	Matches(f string, v string, ignoreCase bool, multiLine bool) Query

	// Add a constraint requiring the location of GeoPoint field specified by f be
	// within the rectangular geographic bounding box with a southwest corner
	// represented by sw and a northeast corner represented by ne
	WithinGeoBox(f string, sw GeoPoint, ne GeoPoint) Query

	// Add a constraint requiring the location of GeoPoint field specified by f
	// be near the point represented by g
	Near(f string, g GeoPoint) Query

	// Add a constraint requiring the location of GeoPoint field specified by f
	// be near the point represented by g with a maximum distance in miles
	// represented by m
	WithinMiles(f string, g GeoPoint, m float64) Query

	// Add a constraint requiring the location of GeoPoint field specified by f
	// be near the point represented by g with a maximum distance in kilometers
	// represented by m
	WithinKilometers(f string, g GeoPoint, k float64) Query

	// Add a constraint requiring the location of GeoPoint field specified by f
	// be near the point represented by g with a maximum distance in radians
	// represented by m
	WithinRadians(f string, g GeoPoint, r float64) Query

	// Add a constraint requiring the value of the field specified by f be equal
	// to the field named qk in the result of the subquery sq
	MatchesKeyInQuery(f string, qk string, sq Query) Query

	// Add a constraint requiring the value of the field specified by f not match
	// the field named qk in the result of the subquery sq
	DoesNotMatchKeyInQuery(f string, qk string, sq Query) Query

	// Add a constraint requiring the field specified by f contain the object
	// returned by Parse query q
	MatchesQuery(f string, q Query) Query

	// Add a constraint requiring the field specified by f not contain the object
	// returned by the Parse query q
	DoesNotMatchQuery(f string, q Query) Query

	// Convenience method for duplicating a query
	Clone() Query

	// Convenience method for building a subquery for use with Query.Or
	Sub() Query

	// Constructs a query where each result must satisfy one of the given
	// subueries
	//
	// E.g.:
	//
	// q, _ := parse.NewQuery(&parse.User{})
	//
	// sq1 := q.Sub().EqualTo("city", "Chicago")
	//
	// sq2 := q.Sub().GreaterThan("age", 30)
	//
	// sq3 := q.Sub().In("occupation", []string{"engineer", "developer"})
	//
	// q.Or(sq1, sq2, sq3)
	// q.Each(...)
	Or(qs ...Query) Query

	// Fetch all results for a query, sending each result to the provided
	// channel rc. The element type of rc should match that of the query,
	// otherwise an error will be returned.
	//
	// Errors are passed to the channel ec. If an error occurns during iteration,
	// iteration will stop
	//
	// The third argument is a channel which may be used for cancelling
	// iteration. Simply send an empty struct value to the channel,
	// and iteration will discontinue. This argument may be nil.
	Each(rc interface{}) (*Iterator, error)

	SetBatchSize(size uint) Query

	// Retrieves a list of objects that satisfy the given query. The results
	// are assigned to the slice provided to NewQuery.
	//
	// E.g.:
	//
	// users := make([]parse.User)
	// q, _ := parse.NewQuery(&users)
	// q.EqualTo("city", "Chicago")
	// q.OrderBy("-createdAt")
	// q.Limit(20)
	// q.Find() // Retrieve the 20 newest users in Chicago
	Find() error

	// Retrieves the first result that satisfies the given query. The result
	// is assigned to the value provided to NewQuery.
	//
	// E.g.:
	// u := parse.User{}
	// q, _ := parse.NewQuery(&u)
	// q.EqualTo("city", "Chicago")
	// q.OrderBy("-createdAt")
	// q.First() // Retrieve the newest user in Chicago
	First() error

	// Retrieve the number of results that satisfy the given query
	Count() (int64, error)
	// contains filtered or unexported methods
}

func NewPushQuery

func NewPushQuery() Query

Convenience function for creating a new query for use in SendPush.

func NewQuery

func NewQuery(v interface{}) (Query, error)

Create a new query instance.

type Session

type Session interface {
	User() interface{}
	NewQuery(v interface{}) (Query, error)
	NewUpdate(v interface{}) (Update, error)
	Create(v interface{}) error
	Delete(v interface{}) error
	CallFunction(name string, params Params, resp interface{}) error
}

func Become

func Become(st string, u interface{}) (Session, error)

Log in as the user identified by the session token st

Optionally provide a custom User type to use in place of parse.User. If user is not nil, it will be populated with the user's attributes, and will be accessible by calling session.User().

func Login

func Login(username, password string, u interface{}) (Session, error)

Login in as the user identified by the provided username and password.

Optionally provide a custom User type to use in place of parse.User. If u is not nil, it will be populated with the user's attributes, and will be accessible by calling session.User().

func LoginFacebook

func LoginFacebook(authData *FacebookAuthData, u interface{}) (Session, error)

type TwitterAuthData

type TwitterAuthData struct {
	Id              string `json:"id"`
	ScreenName      string `json:"screen_name" parse:"screen_name"`
	ConsumerKey     string `json:"consumer_key" parse:"consumer_key"`
	ConsumerSecret  string `json:"consumer_secret" parse:"consumer_secret"`
	AuthToken       string `json:"auth_token" parse:"auth_token"`
	AuthTokenSecret string `json:"auth_token_secret" parse:"auth_token_secret"`
}

type Update

type Update interface {

	//Set the field specified by f to the value of v
	Set(f string, v interface{}) Update

	// Increment the field specified by f by the amount specified by v.
	// v should be a numeric type
	Increment(f string, v interface{}) Update

	// Delete the field specified by f from the instance being updated
	Delete(f string) Update

	// Append the values provided to the Array field specified by f. This operation
	// is atomic
	Add(f string, vs ...interface{}) Update

	// Add any values provided that were not alread present to the Array field
	// specified by f. This operation is atomic
	AddUnique(f string, vs ...interface{}) Update

	// Remove the provided values from the array field specified by f
	Remove(f string, vs ...interface{}) Update

	// Update the ACL on the given object
	SetACL(a ACL) Update

	// Use the Master Key for this update request
	UseMasterKey() Update

	// Execute this update. This method also updates the proper fields
	// on the provided value with their repective new values
	Execute() error
	// contains filtered or unexported methods
}

func NewUpdate

func NewUpdate(v interface{}) (Update, error)

Create a new update request for the Parse object represented by v.

Note: v should be a pointer to a struct whose name represents a Parse class, or that implements the ClassName method

type User

type User struct {
	Base
	Username      string
	Email         string
	EmailVerified bool `json:"-" parse:"-"`
}

Represents the built-in Parse "User" class. Embed this type in a custom type containing any custom fields. When fetching user objects, any retrieved fields with no matching struct field will be stored in User.Extra (map[string]interface{})

func (*User) ClassName

func (u *User) ClassName() string

func (*User) Endpoint

func (u *User) Endpoint() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL