1// Copyright 2017 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package firestore
16
17import (
18	"context"
19	"errors"
20	"fmt"
21	"io"
22	"reflect"
23	"sort"
24
25	vkit "cloud.google.com/go/firestore/apiv1"
26	"cloud.google.com/go/internal/trace"
27	"google.golang.org/api/iterator"
28	pb "google.golang.org/genproto/googleapis/firestore/v1"
29	"google.golang.org/grpc/codes"
30	"google.golang.org/grpc/status"
31)
32
33var errNilDocRef = errors.New("firestore: nil DocumentRef")
34
35// A DocumentRef is a reference to a Firestore document.
36type DocumentRef struct {
37	// The CollectionRef that this document is a part of. Never nil.
38	Parent *CollectionRef
39
40	// The full resource path of the document. A document "doc-1" in collection
41	// "coll-1" would be: "projects/P/databases/D/documents/coll-1/doc-1".
42	Path string
43
44	// The shorter resource path of the document. A document "doc-1" in
45	// collection "coll-1" would be: "coll-1/doc-1".
46	shortPath string
47
48	// The ID of the document: the last component of the resource path.
49	ID string
50}
51
52func newDocRef(parent *CollectionRef, id string) *DocumentRef {
53	return &DocumentRef{
54		Parent:    parent,
55		ID:        id,
56		Path:      parent.Path + "/" + id,
57		shortPath: parent.selfPath + "/" + id,
58	}
59}
60
61// Collection returns a reference to sub-collection of this document.
62func (d *DocumentRef) Collection(id string) *CollectionRef {
63	return newCollRefWithParent(d.Parent.c, d, id)
64}
65
66// Get retrieves the document. If the document does not exist, Get return a NotFound error, which
67// can be checked with
68//    grpc.Code(err) == codes.NotFound
69// In that case, Get returns a non-nil DocumentSnapshot whose Exists method return false and whose
70// ReadTime is the time of the failed read operation.
71func (d *DocumentRef) Get(ctx context.Context) (_ *DocumentSnapshot, err error) {
72	ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.Get")
73	defer func() { trace.EndSpan(ctx, err) }()
74
75	if d == nil {
76		return nil, errNilDocRef
77	}
78	docsnaps, err := d.Parent.c.getAll(ctx, []*DocumentRef{d}, nil)
79	if err != nil {
80		return nil, err
81	}
82	ds := docsnaps[0]
83	if !ds.Exists() {
84		return ds, status.Errorf(codes.NotFound, "%q not found", d.Path)
85	}
86	return ds, nil
87}
88
89// Create creates the document with the given data.
90// It returns an error if a document with the same ID already exists.
91//
92// The data argument can be a map with string keys, a struct, or a pointer to a
93// struct. The map keys or exported struct fields become the fields of the firestore
94// document.
95// The values of data are converted to Firestore values as follows:
96//
97//   - bool converts to Bool.
98//   - string converts to String.
99//   - int, int8, int16, int32 and int64 convert to Integer.
100//   - uint8, uint16 and uint32 convert to Integer. uint, uint64 and uintptr are disallowed,
101//     because they may be able to represent values that cannot be represented in an int64,
102//     which is the underlying type of a Integer.
103//   - float32 and float64 convert to Double.
104//   - []byte converts to Bytes.
105//   - time.Time and *ts.Timestamp convert to Timestamp. ts is the package
106//     "github.com/golang/protobuf/ptypes/timestamp".
107//   - *latlng.LatLng converts to GeoPoint. latlng is the package
108//     "google.golang.org/genproto/googleapis/type/latlng". You should always use
109//     a pointer to a LatLng.
110//   - Slices convert to Array.
111//   - *firestore.DocumentRef converts to Reference.
112//   - Maps and structs convert to Map.
113//   - nils of any type convert to Null.
114//
115// Pointers and interface{} are also permitted, and their elements processed
116// recursively.
117//
118// Struct fields can have tags like those used by the encoding/json package. Tags
119// begin with "firestore:" and are followed by "-", meaning "ignore this field," or
120// an alternative name for the field. Following the name, these comma-separated
121// options may be provided:
122//
123//   - omitempty: Do not encode this field if it is empty. A value is empty
124//     if it is a zero value, or an array, slice or map of length zero.
125//   - serverTimestamp: The field must be of type time.Time. When writing, if
126//     the field has the zero value, the server will populate the stored document with
127//     the time that the request is processed.
128func (d *DocumentRef) Create(ctx context.Context, data interface{}) (_ *WriteResult, err error) {
129	ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.Create")
130	defer func() { trace.EndSpan(ctx, err) }()
131
132	ws, err := d.newCreateWrites(data)
133	if err != nil {
134		return nil, err
135	}
136	return d.Parent.c.commit1(ctx, ws)
137}
138
139func (d *DocumentRef) newCreateWrites(data interface{}) ([]*pb.Write, error) {
140	if d == nil {
141		return nil, errNilDocRef
142	}
143	doc, transforms, err := toProtoDocument(data)
144	if err != nil {
145		return nil, err
146	}
147	doc.Name = d.Path
148	pc, err := exists(false).preconditionProto()
149	if err != nil {
150		return nil, err
151	}
152	return d.newUpdateWithTransform(doc, nil, pc, transforms, false), nil
153}
154
155// Set creates or overwrites the document with the given data. See DocumentRef.Create
156// for the acceptable values of data. Without options, Set overwrites the document
157// completely. Specify one of the Merge options to preserve an existing document's
158// fields. To delete some fields, use a Merge option with firestore.Delete as the
159// field value.
160func (d *DocumentRef) Set(ctx context.Context, data interface{}, opts ...SetOption) (_ *WriteResult, err error) {
161	ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.Set")
162	defer func() { trace.EndSpan(ctx, err) }()
163
164	ws, err := d.newSetWrites(data, opts)
165	if err != nil {
166		return nil, err
167	}
168	return d.Parent.c.commit1(ctx, ws)
169}
170
171func (d *DocumentRef) newSetWrites(data interface{}, opts []SetOption) ([]*pb.Write, error) {
172	if d == nil {
173		return nil, errNilDocRef
174	}
175	if data == nil {
176		return nil, errors.New("firestore: nil document contents")
177	}
178	if len(opts) == 0 { // Set without merge
179		doc, serverTimestampPaths, err := toProtoDocument(data)
180		if err != nil {
181			return nil, err
182		}
183		doc.Name = d.Path
184		return d.newUpdateWithTransform(doc, nil, nil, serverTimestampPaths, true), nil
185	}
186	// Set with merge.
187	// This is just like Update, except for the existence precondition.
188	// So we turn data into a list of (FieldPath, interface{}) pairs (fpv's), as we do
189	// for Update.
190	fieldPaths, allPaths, err := processSetOptions(opts)
191	if err != nil {
192		return nil, err
193	}
194	var fpvs []fpv
195	v := reflect.ValueOf(data)
196	if allPaths {
197		// Set with MergeAll. Collect all the leaves of the map.
198		if v.Kind() != reflect.Map {
199			return nil, errors.New("firestore: MergeAll can only be specified with map data")
200		}
201		if v.Len() == 0 {
202			// Special case: MergeAll with an empty map.
203			return d.newUpdateWithTransform(&pb.Document{Name: d.Path}, []FieldPath{}, nil, nil, true), nil
204		}
205		fpvsFromData(v, nil, &fpvs)
206	} else {
207		// Set with merge paths.  Collect only the values at the given paths.
208		for _, fp := range fieldPaths {
209			val, err := getAtPath(v, fp)
210			if err != nil {
211				return nil, err
212			}
213			fpvs = append(fpvs, fpv{fp, val})
214		}
215	}
216	return d.fpvsToWrites(fpvs, nil)
217}
218
219// fpvsFromData converts v into a list of (FieldPath, value) pairs.
220func fpvsFromData(v reflect.Value, prefix FieldPath, fpvs *[]fpv) {
221	switch v.Kind() {
222	case reflect.Map:
223		for _, k := range v.MapKeys() {
224			fpvsFromData(v.MapIndex(k), prefix.with(k.String()), fpvs)
225		}
226	case reflect.Interface:
227		fpvsFromData(v.Elem(), prefix, fpvs)
228
229	default:
230		var val interface{}
231		if v.IsValid() {
232			val = v.Interface()
233		}
234		*fpvs = append(*fpvs, fpv{prefix, val})
235	}
236}
237
238// Delete deletes the document. If the document doesn't exist, it does nothing
239// and returns no error.
240func (d *DocumentRef) Delete(ctx context.Context, preconds ...Precondition) (_ *WriteResult, err error) {
241	ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.Delete")
242	defer func() { trace.EndSpan(ctx, err) }()
243
244	ws, err := d.newDeleteWrites(preconds)
245	if err != nil {
246		return nil, err
247	}
248	return d.Parent.c.commit1(ctx, ws)
249}
250
251func (d *DocumentRef) newDeleteWrites(preconds []Precondition) ([]*pb.Write, error) {
252	if d == nil {
253		return nil, errNilDocRef
254	}
255	pc, err := processPreconditionsForDelete(preconds)
256	if err != nil {
257		return nil, err
258	}
259	return []*pb.Write{{
260		Operation:       &pb.Write_Delete{d.Path},
261		CurrentDocument: pc,
262	}}, nil
263}
264
265func (d *DocumentRef) newUpdatePathWrites(updates []Update, preconds []Precondition) ([]*pb.Write, error) {
266	if len(updates) == 0 {
267		return nil, errors.New("firestore: no paths to update")
268	}
269	var fpvs []fpv
270	for _, u := range updates {
271		v, err := u.process()
272		if err != nil {
273			return nil, err
274		}
275		fpvs = append(fpvs, v)
276	}
277	pc, err := processPreconditionsForUpdate(preconds)
278	if err != nil {
279		return nil, err
280	}
281	return d.fpvsToWrites(fpvs, pc)
282}
283
284func (d *DocumentRef) fpvsToWrites(fpvs []fpv, pc *pb.Precondition) ([]*pb.Write, error) {
285	// Make sure there are no duplications or prefixes among the field paths.
286	var fps []FieldPath
287	for _, fpv := range fpvs {
288		fps = append(fps, fpv.fieldPath)
289	}
290	if err := checkNoDupOrPrefix(fps); err != nil {
291		return nil, err
292	}
293
294	// Process each fpv.
295	var updatePaths []FieldPath
296	var transforms []*pb.DocumentTransform_FieldTransform
297	doc := &pb.Document{
298		Name:   d.Path,
299		Fields: map[string]*pb.Value{},
300	}
301	for _, fpv := range fpvs {
302		switch fpv.value.(type) {
303		case arrayUnion:
304			au := fpv.value.(arrayUnion)
305			t, err := arrayUnionTransform(au, fpv.fieldPath)
306			if err != nil {
307				return nil, err
308			}
309			transforms = append(transforms, t)
310		case arrayRemove:
311			ar := fpv.value.(arrayRemove)
312			t, err := arrayRemoveTransform(ar, fpv.fieldPath)
313			if err != nil {
314				return nil, err
315			}
316			transforms = append(transforms, t)
317		default:
318			switch fpv.value {
319			case Delete:
320				// Send the field path without a corresponding value.
321				updatePaths = append(updatePaths, fpv.fieldPath)
322
323			case ServerTimestamp:
324				// Use the path in a transform operation.
325				transforms = append(transforms, serverTimestamp(fpv.fieldPath.toServiceFieldPath()))
326
327			default:
328				updatePaths = append(updatePaths, fpv.fieldPath)
329				// Convert the value to a proto and put it into the document.
330				v := reflect.ValueOf(fpv.value)
331
332				pv, _, err := toProtoValue(v)
333				if err != nil {
334					return nil, err
335				}
336				setAtPath(doc.Fields, fpv.fieldPath, pv)
337				// Also accumulate any transforms within the value.
338				ts, err := extractTransforms(v, fpv.fieldPath)
339				if err != nil {
340					return nil, err
341				}
342				transforms = append(transforms, ts...)
343			}
344		}
345	}
346	return d.newUpdateWithTransform(doc, updatePaths, pc, transforms, false), nil
347}
348
349// newUpdateWithTransform constructs operations for a commit. Most generally, it
350// returns an update operation followed by a transform.
351//
352// If there are no serverTimestampPaths, the transform is omitted.
353//
354// If doc.Fields is empty, there are no updatePaths, and there is no precondition,
355// the update is omitted, unless updateOnEmpty is true.
356func (d *DocumentRef) newUpdateWithTransform(doc *pb.Document, updatePaths []FieldPath, pc *pb.Precondition, transforms []*pb.DocumentTransform_FieldTransform, updateOnEmpty bool) []*pb.Write {
357	var ws []*pb.Write
358	if updateOnEmpty || len(doc.Fields) > 0 ||
359		len(updatePaths) > 0 || (pc != nil && len(transforms) == 0) {
360		var mask *pb.DocumentMask
361		if updatePaths != nil {
362			sfps := toServiceFieldPaths(updatePaths)
363			sort.Strings(sfps) // TODO(jba): make tests pass without this
364			mask = &pb.DocumentMask{FieldPaths: sfps}
365		}
366		w := &pb.Write{
367			Operation:       &pb.Write_Update{doc},
368			UpdateMask:      mask,
369			CurrentDocument: pc,
370		}
371		ws = append(ws, w)
372		pc = nil // If the precondition is in the write, we don't need it in the transform.
373	}
374	if len(transforms) > 0 || pc != nil {
375		ws = append(ws, &pb.Write{
376			Operation: &pb.Write_Transform{
377				Transform: &pb.DocumentTransform{
378					Document:        d.Path,
379					FieldTransforms: transforms,
380				},
381			},
382			CurrentDocument: pc,
383		})
384	}
385	return ws
386}
387
388// arrayUnion is a special type in firestore. It instructs the server to add its
389// elements to whatever array already exists, or to create an array if no value
390// exists.
391type arrayUnion struct {
392	elems []interface{}
393}
394
395// ArrayUnion specifies elements to be added to whatever array already exists in
396// the server, or to create an array if no value exists.
397//
398// If a value exists and it's an array, values are appended to it. Any duplicate
399// value is ignored.
400// If a value exists and it's not an array, the value is replaced by an array of
401// the values in the ArrayUnion.
402// If a value does not exist, an array of the values in the ArrayUnion is created.
403//
404// ArrayUnion must be the value of a field directly; it cannot appear in
405// array or struct values, or in any value that is itself inside an array or
406// struct.
407func ArrayUnion(elems ...interface{}) arrayUnion {
408	return arrayUnion{elems: elems}
409}
410
411// This helper converts an arrayUnion into a proto object.
412func arrayUnionTransform(au arrayUnion, fp FieldPath) (*pb.DocumentTransform_FieldTransform, error) {
413	var elems []*pb.Value
414	for _, v := range au.elems {
415		pv, _, err := toProtoValue(reflect.ValueOf(v))
416		if err != nil {
417			return nil, err
418		}
419		elems = append(elems, pv)
420	}
421	return &pb.DocumentTransform_FieldTransform{
422		FieldPath: fp.toServiceFieldPath(),
423		TransformType: &pb.DocumentTransform_FieldTransform_AppendMissingElements{
424			AppendMissingElements: &pb.ArrayValue{Values: elems},
425		},
426	}, nil
427}
428
429// arrayRemove is a special type in firestore. It instructs the server to remove
430// the specified values.
431type arrayRemove struct {
432	elems []interface{}
433}
434
435// ArrayRemove specifies elements to be removed from whatever array already
436// exists in the server.
437//
438// If a value exists and it's an array, values are removed from it. All
439// duplicate values are removed.
440// If a value exists and it's not an array, the value is replaced by an empty
441// array.
442// If a value does not exist, an empty array is created.
443//
444// ArrayRemove must be the value of a field directly; it cannot appear in
445// array or struct values, or in any value that is itself inside an array or
446// struct.
447func ArrayRemove(elems ...interface{}) arrayRemove {
448	return arrayRemove{elems: elems}
449}
450
451// This helper converts an arrayRemove into a proto object.
452func arrayRemoveTransform(ar arrayRemove, fp FieldPath) (*pb.DocumentTransform_FieldTransform, error) {
453	var elems []*pb.Value
454	for _, v := range ar.elems {
455		// ServerTimestamp cannot occur in an array, so we ignore transformations here.
456		pv, _, err := toProtoValue(reflect.ValueOf(v))
457		if err != nil {
458			return nil, err
459		}
460		elems = append(elems, pv)
461	}
462	return &pb.DocumentTransform_FieldTransform{
463		FieldPath: fp.toServiceFieldPath(),
464		TransformType: &pb.DocumentTransform_FieldTransform_RemoveAllFromArray{
465			RemoveAllFromArray: &pb.ArrayValue{Values: elems},
466		},
467	}, nil
468}
469
470type sentinel int
471
472const (
473	// Delete is used as a value in a call to Update or Set with merge to indicate
474	// that the corresponding key should be deleted.
475	Delete sentinel = iota
476
477	// ServerTimestamp is used as a value in a call to Update to indicate that the
478	// key's value should be set to the time at which the server processed
479	// the request.
480	//
481	// ServerTimestamp must be the value of a field directly; it cannot appear in
482	// array or struct values, or in any value that is itself inside an array or
483	// struct.
484	ServerTimestamp
485)
486
487func (s sentinel) String() string {
488	switch s {
489	case Delete:
490		return "Delete"
491	case ServerTimestamp:
492		return "ServerTimestamp"
493	default:
494		return "<?sentinel?>"
495	}
496}
497
498// An Update describes an update to a value referred to by a path.
499// An Update should have either a non-empty Path or a non-empty FieldPath,
500// but not both.
501//
502// See DocumentRef.Create for acceptable values.
503// To delete a field, specify firestore.Delete as the value.
504type Update struct {
505	Path      string // Will be split on dots, and must not contain any of "˜*/[]".
506	FieldPath FieldPath
507	Value     interface{}
508}
509
510// An fpv is a pair of validated FieldPath and value.
511type fpv struct {
512	fieldPath FieldPath
513	value     interface{}
514}
515
516func (u *Update) process() (fpv, error) {
517	if (u.Path != "") == (u.FieldPath != nil) {
518		return fpv{}, fmt.Errorf("firestore: update %+v should have exactly one of Path or FieldPath", u)
519	}
520	fp := u.FieldPath
521	var err error
522	if fp == nil {
523		fp, err = parseDotSeparatedString(u.Path)
524		if err != nil {
525			return fpv{}, err
526		}
527	}
528	if err := fp.validate(); err != nil {
529		return fpv{}, err
530	}
531	return fpv{fp, u.Value}, nil
532}
533
534// Update updates the document. The values at the given
535// field paths are replaced, but other fields of the stored document are untouched.
536func (d *DocumentRef) Update(ctx context.Context, updates []Update, preconds ...Precondition) (_ *WriteResult, err error) {
537	ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.Update")
538	defer func() { trace.EndSpan(ctx, err) }()
539
540	ws, err := d.newUpdatePathWrites(updates, preconds)
541	if err != nil {
542		return nil, err
543	}
544	return d.Parent.c.commit1(ctx, ws)
545}
546
547// Collections returns an iterator over the immediate sub-collections of the document.
548func (d *DocumentRef) Collections(ctx context.Context) *CollectionIterator {
549	client := d.Parent.c
550	it := &CollectionIterator{
551		client: client,
552		parent: d,
553		it: client.c.ListCollectionIds(
554			withResourceHeader(ctx, client.path()),
555			&pb.ListCollectionIdsRequest{Parent: d.Path}),
556	}
557	it.pageInfo, it.nextFunc = iterator.NewPageInfo(
558		it.fetch,
559		func() int { return len(it.items) },
560		func() interface{} { b := it.items; it.items = nil; return b })
561	return it
562}
563
564// CollectionIterator is an iterator over sub-collections of a document.
565type CollectionIterator struct {
566	client   *Client
567	parent   *DocumentRef
568	it       *vkit.StringIterator
569	pageInfo *iterator.PageInfo
570	nextFunc func() error
571	items    []*CollectionRef
572	err      error
573}
574
575// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
576func (it *CollectionIterator) PageInfo() *iterator.PageInfo { return it.pageInfo }
577
578// Next returns the next result. Its second return value is iterator.Done if there
579// are no more results. Once Next returns Done, all subsequent calls will return
580// Done.
581func (it *CollectionIterator) Next() (*CollectionRef, error) {
582	if err := it.nextFunc(); err != nil {
583		return nil, err
584	}
585	item := it.items[0]
586	it.items = it.items[1:]
587	return item, nil
588}
589
590func (it *CollectionIterator) fetch(pageSize int, pageToken string) (string, error) {
591	if it.err != nil {
592		return "", it.err
593	}
594	return iterFetch(pageSize, pageToken, it.it.PageInfo(), func() error {
595		id, err := it.it.Next()
596		if err != nil {
597			return err
598		}
599		var cr *CollectionRef
600		if it.parent == nil {
601			cr = newTopLevelCollRef(it.client, it.client.path(), id)
602		} else {
603			cr = newCollRefWithParent(it.client, it.parent, id)
604		}
605		it.items = append(it.items, cr)
606		return nil
607	})
608}
609
610// GetAll returns all the collections remaining from the iterator.
611func (it *CollectionIterator) GetAll() ([]*CollectionRef, error) {
612	var crs []*CollectionRef
613	for {
614		cr, err := it.Next()
615		if err == iterator.Done {
616			break
617		}
618		if err != nil {
619			return nil, err
620		}
621		crs = append(crs, cr)
622	}
623	return crs, nil
624}
625
626// Common fetch code for iterators that are backed by vkit iterators.
627// TODO(jba): dedup with same function in logging/logadmin.
628func iterFetch(pageSize int, pageToken string, pi *iterator.PageInfo, next func() error) (string, error) {
629	pi.MaxSize = pageSize
630	pi.Token = pageToken
631	// Get one item, which will fill the buffer.
632	if err := next(); err != nil {
633		return "", err
634	}
635	// Collect the rest of the buffer.
636	for pi.Remaining() > 0 {
637		if err := next(); err != nil {
638			return "", err
639		}
640	}
641	return pi.Token, nil
642}
643
644// Snapshots returns an iterator over snapshots of the document. Each time the document
645// changes or is added or deleted, a new snapshot will be generated.
646func (d *DocumentRef) Snapshots(ctx context.Context) *DocumentSnapshotIterator {
647	return &DocumentSnapshotIterator{
648		docref: d,
649		ws:     newWatchStreamForDocument(ctx, d),
650	}
651}
652
653// DocumentSnapshotIterator is an iterator over snapshots of a document.
654// Call Next on the iterator to get a snapshot of the document each time it changes.
655// Call Stop on the iterator when done.
656//
657// For an example, see DocumentRef.Snapshots.
658type DocumentSnapshotIterator struct {
659	docref *DocumentRef
660	ws     *watchStream
661}
662
663// Next blocks until the document changes, then returns the DocumentSnapshot for
664// the current state of the document. If the document has been deleted, Next
665// returns a DocumentSnapshot whose Exists method returns false.
666//
667// Next never returns iterator.Done unless it is called after Stop.
668func (it *DocumentSnapshotIterator) Next() (*DocumentSnapshot, error) {
669	btree, _, readTime, err := it.ws.nextSnapshot()
670	if err != nil {
671		if err == io.EOF {
672			err = iterator.Done
673		}
674		// watchStream's error is sticky, so SnapshotIterator does not need to remember it.
675		return nil, err
676	}
677	if btree.Len() == 0 { // document deleted
678		return &DocumentSnapshot{Ref: it.docref, ReadTime: readTime}, nil
679	}
680	snap, _ := btree.At(0)
681	return snap.(*DocumentSnapshot), nil
682}
683
684// Stop stops receiving snapshots. You should always call Stop when you are done with
685// a DocumentSnapshotIterator, to free up resources. It is not safe to call Stop
686// concurrently with Next.
687func (it *DocumentSnapshotIterator) Stop() {
688	it.ws.stop()
689}
690