Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39537
1
###############################################################################
2
#
3
# CoCalc: Collaborative Calculation in the Cloud
4
#
5
# Copyright (C) 2016, Sagemath Inc.
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#
20
###############################################################################
21
22
23
###
24
The schema below determines the PostgreSQL database schema. The notation is as follows:
25
26
schema.table_name =
27
desc: 'A description of this table.' # will be used only for tooling
28
primary_key : 'the_table_primary_key'
29
durability : 'hard' or 'soft' # optional -- if given, specify the table durability; 'hard' is the default
30
fields : # every field *must* be listed here or user queries won't work.
31
the_table_primary_key :
32
type : 'uuid'
33
desc : 'This is the primary key of the table.'
34
...
35
pg_indexes : [array of column names] # also some more complicated ways to define indexes; see the examples.
36
user_query : # queries that are directly exposed to the client via a friendly "fill in what result looks like" query language
37
get : # describes get query for reading data from this table
38
pg_where : # this gets run first on the table before
39
'account_id' - replaced by user's account_id
40
'project_id' - filled in by project_id, which must be specified in the query itself;
41
(if table not anonymous then project_id must be a project that user has read access to)
42
'project_id-public' - filled in by project_id, which must be specified in the query itself;
43
(if table not anonymous then project_id must be of a project with at east one public path)
44
'all_projects_read' - filled in with list of all the id's of projects this user has read access to
45
'collaborators' - filled in by account_id's of all collaborators of this user
46
an arbitrary function - gets called with an object with these keys:
47
account_id, table, query, multi, options, changes
48
fields : # these are the fields any user is allowed to see, subject to the all constraint above
49
field_name : either null or a default_value
50
another_field : 10 # means will default to 10 if undefined in database
51
this_field : null # no default filled in
52
settings :
53
strip : false # defaults for a field that is an object -- these get filled in if missing in db
54
wrap : true
55
set : # describes more dangerous *set* queries that the user can make via the query language
56
pg_where : # initially restrict what user can set
57
'account_id' - user account_id
58
- list of project_id's that the user has write access to
59
fields : # user must always give the primary key in set queries
60
account_id : 'account_id' # means that this field will automatically be filled in with account_id
61
project_id : 'project_write' # means that this field *must* be a project_id that the user has *write* access to
62
foo : true # user is allowed (but not required) to set this
63
bar : true # means user is allowed to set this
64
65
To specify more than one user query against a table, make a new table as above, omitting
66
everything except the user_query section, and include a virtual section listing the actual
67
table to query:
68
69
virtual : 'original_table'
70
71
For example,
72
73
schema.collaborators =
74
primary_key : 'account_id'
75
anonymous : false
76
virtual : 'accounts'
77
user_query:
78
get : ...
79
80
81
Finally, putting
82
83
anonymous : true
84
85
makes it so non-signed-in-users may query the table (read only) for data, e.g.,
86
87
schema.stats =
88
primary_key: 'id'
89
anonymous : true # allow user access, even if not signed in
90
fields:
91
id : true
92
...
93
94
###
95
96
misc = require('./misc')
97
98
{DEFAULT_QUOTAS} = require('./upgrade-spec')
99
100
schema = exports.SCHEMA = {}
101
102
schema.account_creation_actions =
103
desc : 'Actions to carry out when accounts are created, triggered by the email address of the user.'
104
primary_key : 'id'
105
fields :
106
id :
107
type : 'uuid'
108
action :
109
type : 'map'
110
desc : 'Describes the action to carry out when an account is created with the given email_address.'
111
email_address :
112
type : 'string'
113
desc : 'Email address of user.'
114
expire :
115
type : 'timestamp'
116
desc : 'When this action should be expired.'
117
pg_indexes : ['email_address']
118
119
schema.accounts =
120
desc : 'All user accounts.'
121
primary_key : 'account_id'
122
fields :
123
account_id :
124
type : 'uuid',
125
desc : 'The uuid that determines the user account'
126
created :
127
type : 'timestamp'
128
desc : 'When the account was created.'
129
created_by :
130
type : 'string'
131
pg_type : 'inet'
132
desc : 'IP address that created the account.'
133
creation_actions_done :
134
type : 'boolean'
135
desc : 'Set to true after all creation actions (e.g., add to projects) associated to this account are succesfully completed.'
136
password_hash :
137
type : 'string'
138
pg_type : 'VARCHAR(173)'
139
desc : 'hash of the password'
140
deleted :
141
type : 'boolean'
142
desc : "True if the account has been deleted."
143
email_address :
144
type : 'string'
145
pg_type : "VARCHAR(254)" # see http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
146
desc : 'The email address of the user. This is optional, since users may instead be associated to passport logins.'
147
unique : true # only one record in database can have this email address (if given)
148
email_address_before_delete :
149
type : 'string'
150
desc : 'The email address of the user before they deleted their account.'
151
passports :
152
type : 'map'
153
desc : 'Map from string ("[strategy]-[id]") derived from passport name and id to the corresponding profile'
154
editor_settings :
155
type : 'map'
156
desc : 'Description of configuration settings for the editor. See the user_query get defaults.'
157
other_settings :
158
type : 'map'
159
desc : 'Miscellaneous overall configuration settings for SMC, e.g., confirm close on exit?'
160
first_name :
161
type : 'string'
162
pg_type : "VARCHAR(254)" # some limit (actually around 3000) is required for indexing
163
desc : 'The first name of this user.'
164
last_name :
165
type : 'string'
166
pg_type : "VARCHAR(254)"
167
desc : 'The last name of this user.'
168
banned :
169
type : 'boolean'
170
desc : 'Whether or not this user is banned.'
171
terminal :
172
type : 'map'
173
desc : 'Settings for the terminal, e.g., font_size, etc. (see get query)'
174
autosave :
175
type : 'integer'
176
desc : 'File autosave interval in seconds'
177
evaluate_key :
178
type : 'string'
179
desc : 'Key used to evaluate code in Sage worksheet.'
180
font_size :
181
type : 'integer'
182
desc : 'Default font-size for the editor, jupyter, etc. (px)'
183
last_active :
184
type : 'timestamp'
185
desc : 'When this user was last active.'
186
stripe_customer_id :
187
type : 'string'
188
desc : 'The id of this customer in the stripe billing system.'
189
stripe_customer :
190
type : 'map'
191
desc : 'Information about customer from the point of view of stripe (exactly what is returned by stripe.customers.retrieve).'
192
profile :
193
type : 'map'
194
desc : 'Information related to displaying this users location and presence in a document or chatroom.'
195
groups :
196
type : 'array'
197
pg_type : 'TEXT[]'
198
desc : "Array of groups that this user belongs to; usually empty. The only group right now is 'admin', which grants admin rights."
199
ssh_keys :
200
type : 'map'
201
desc : 'Map from ssh key fingerprints to ssh key objects.'
202
api_key :
203
type : 'string'
204
desc : "Optional API key that grants full API access to anything this account can access. Key is of the form 'sk_9QabcrqJFy7JIhvAGih5c6Nb', where the random part is 24 characters (base 62)."
205
pg_indexes : [
206
'(lower(first_name) text_pattern_ops)',
207
'(lower(last_name) text_pattern_ops)',
208
'created_by',
209
'created',
210
'api_key'
211
]
212
user_query :
213
get :
214
throttle_changes : 500
215
pg_where : ['account_id = $::UUID':'account_id']
216
fields :
217
account_id : null
218
email_address : null
219
editor_settings :
220
strip_trailing_whitespace : false
221
show_trailing_whitespace : true
222
line_wrapping : true
223
line_numbers : true
224
smart_indent : true
225
electric_chars : true
226
match_brackets : true
227
auto_close_brackets : true
228
code_folding : true
229
match_xml_tags : true
230
auto_close_xml_tags : true
231
spaces_instead_of_tabs : true
232
multiple_cursors : true
233
track_revisions : true
234
extra_button_bar : true
235
first_line_number : 1
236
indent_unit : 4
237
tab_size : 4
238
bindings : "standard"
239
theme : "default"
240
undo_depth : 300
241
other_settings :
242
confirm_close : false
243
mask_files : true
244
page_size : 50
245
standby_timeout_m : 10
246
default_file_sort : 'time'
247
show_global_info2 : null
248
first_name : ''
249
last_name : ''
250
terminal :
251
font_size : 14
252
color_scheme : 'default'
253
font : 'monospace'
254
autosave : 45
255
evaluate_key : 'Shift-Enter'
256
font_size : 14
257
passports : {}
258
groups : []
259
last_active : null
260
stripe_customer : null
261
profile :
262
image : undefined
263
color : undefined
264
ssh_keys : {}
265
set :
266
fields :
267
account_id : 'account_id'
268
editor_settings : true
269
other_settings : true
270
first_name : true
271
last_name : true
272
terminal : true
273
autosave : true
274
evaluate_key : true
275
font_size : true
276
profile : true
277
ssh_keys : true
278
check_hook : (db, obj, account_id, project_id, cb) ->
279
# Hook to truncate some text fields to at most 254 characters, to avoid
280
# further trouble down the line.
281
for field in ['first_name', 'last_name', 'email_address']
282
if obj[field]?
283
obj[field] = obj[field].slice(0,254)
284
cb()
285
286
schema.blobs =
287
desc : 'Table that stores blobs mainly generated as output of Sage worksheets.'
288
primary_key : 'id'
289
fields :
290
id :
291
type : 'uuid'
292
desc : 'The uuid of this blob, which is a uuid derived from the Sha1 hash of the blob content.'
293
blob :
294
type : 'Buffer'
295
desc : 'The actual blob content'
296
expire :
297
type : 'timestamp'
298
desc : 'When to expire this blob (when delete_expired is called on the database).'
299
created :
300
type : 'timestamp'
301
desc : 'When the blob was created.'
302
project_id :
303
type : 'string'
304
desc : 'The uuid of the project that created the blob.'
305
last_active :
306
type : 'timestamp'
307
desc : 'When the blob was last pulled from the database.'
308
count :
309
type : 'number'
310
desc : 'How many times the blob has been pulled from the database.'
311
size :
312
type : 'number'
313
desc : 'The size in bytes of the blob.'
314
gcloud :
315
type : 'string'
316
desc : 'name of a bucket that contains the actual blob, if available.'
317
backup :
318
type : 'boolean'
319
desc : 'if true, then this blob was saved to an offsite backup'
320
compress :
321
type : 'string'
322
desc : "optional compression used: 'gzip', 'zlib', 'snappy'"
323
user_query :
324
get :
325
instead_of_query : (database, obj, account_id, cb) ->
326
if not obj.id?
327
cb("id must be specified")
328
return
329
database.get_blob
330
uuid : obj.id
331
cb : (err, blob) ->
332
if err
333
cb(err)
334
else
335
cb(undefined, {id:obj.id, blob:blob})
336
fields :
337
id : null
338
blob : null
339
set :
340
fields :
341
id : true
342
blob : true
343
project_id : 'project_write'
344
ttl : 0
345
required_fields :
346
id : true
347
blob : true
348
project_id : true
349
instead_of_change : (database, old_val, new_val, account_id, cb) ->
350
database.save_blob
351
uuid : new_val.id
352
blob : new_val.blob
353
ttl : new_val.ttl
354
project_id : new_val.project_id
355
check : true # can't trust the user!
356
cb : cb
357
358
schema.central_log =
359
desc : 'Table for logging system stuff that happens. Meant to help in running and understanding the system better.'
360
primary_key : 'id'
361
durability : 'soft' # loss of some log data not serious, since used only for analytics
362
fields :
363
id :
364
type : 'uuid'
365
event :
366
type : 'string'
367
value :
368
type : 'map'
369
time :
370
type : 'timestamp'
371
pg_indexes : ['time', 'event']
372
373
schema.client_error_log =
374
primary_key : 'id'
375
durability : 'soft' # loss of some log data not serious, since used only for analytics
376
fields:
377
id :
378
type : 'uuid'
379
event :
380
type : 'string'
381
error :
382
type : 'string'
383
account_id :
384
type : 'uuid'
385
time :
386
type : 'timestamp'
387
pg_indexes : ['time', 'event']
388
389
schema.webapp_errors =
390
primary_key : 'id'
391
durability : 'soft' # loss of some log data not serious, since used only for analytics
392
fields:
393
id : type : 'uuid'
394
account_id : type : 'uuid'
395
name : type : 'string'
396
message : type : 'string'
397
comment : type : 'string'
398
stacktrace : type : 'string'
399
file : type : 'string'
400
path : type : 'string'
401
lineNumber : type : 'integer'
402
columnNumber : type : 'integer'
403
severity : type : 'string'
404
browser : type : 'string'
405
mobile : type : 'boolean'
406
responsive : type : 'boolean'
407
user_agent : type : 'string'
408
path : type : 'text'
409
smc_version : type : 'string'
410
build_date : type : 'string'
411
smc_git_rev : type : 'string'
412
uptime : type : 'string'
413
start_time : type : 'timestamp'
414
time : type : 'timestamp'
415
pg_indexes : ['time', 'name', 'account_id', 'smc_git_rev', 'smc_version', 'start_time', 'browser']
416
417
schema.collaborators =
418
primary_key : 'account_id'
419
anonymous : false
420
virtual : 'accounts'
421
user_query:
422
get :
423
pg_where : ["account_id = ANY(SELECT DISTINCT jsonb_object_keys(users)::UUID FROM projects WHERE users ? $::TEXT)": 'account_id']
424
pg_changefeed : 'collaborators'
425
fields :
426
account_id : null
427
first_name : ''
428
last_name : ''
429
last_active : null
430
profile : null
431
432
schema.compute_servers =
433
primary_key : 'host'
434
fields :
435
host :
436
type : 'string'
437
pg_type : 'VARCHAR(63)'
438
dc :
439
type : 'string'
440
port :
441
type : 'integer'
442
secret :
443
type : 'string'
444
experimental :
445
type : 'boolean'
446
member_host :
447
type : 'boolean'
448
status :
449
type : 'map'
450
desc : 'something like {stuff:?,...,timestamp:?}'
451
date : ['timestamp']
452
453
schema.file_access_log =
454
primary_key : 'id'
455
durability : 'soft' # loss of some log data not serious, since used only for analytics
456
fields:
457
id :
458
type : 'uuid'
459
project_id :
460
type : 'uuid'
461
account_id :
462
type : 'uuid'
463
filename :
464
type : 'string'
465
time :
466
type : 'timestamp'
467
pg_indexes : ['project_id', 'account_id', 'filename', 'time']
468
469
# TODO: for postgres rewrite after done we MIGHT completely redo file_use to eliminate
470
# the id field, use project_id, path as a compound primary key, and maybe put users in
471
# another table with a relation. There is also expert discussion about this table in the
472
# Hacker News discussion of my PostgreSQL vs ... blog post.
473
schema.file_use =
474
primary_key: 'id'
475
durability : 'soft' # loss of some log data not serious, since used only for showing notifications
476
unique_writes: true # there is no reason for a user to write the same record twice
477
fields:
478
id :
479
type : 'string'
480
pg_type : 'CHAR(40)'
481
project_id :
482
type : 'uuid'
483
path :
484
type : 'string'
485
users :
486
type : 'map'
487
desc : '{account_id1: {action1: timestamp1, action2:timestamp2}, account_id2: {...}}'
488
date : 'all'
489
last_edited :
490
type : 'timestamp'
491
492
pg_indexes : ['project_id', 'last_edited']
493
494
user_query:
495
get :
496
pg_where : ['projects', 'last_edited IS NOT NULL']
497
pg_changefeed: 'projects'
498
options : [{order_by : '-last_edited'}, {limit : 200}] # limit is kind of arbitrary; not sure what to do.
499
throttle_changes : 3000
500
fields :
501
id : null
502
project_id : null
503
path : null
504
users : null
505
last_edited : null
506
set :
507
fields :
508
id : (obj, db) -> db.sha1(obj.project_id, obj.path)
509
project_id : 'project_write'
510
path : true
511
users : true
512
last_edited : true
513
required_fields :
514
id : true
515
project_id : true
516
path : true
517
check_hook : (db, obj, account_id, project_id, cb) ->
518
# hook to note that project is being used (CRITICAL: do not pass path
519
# into db.touch since that would cause another write to the file_use table!)
520
# CRITICAL: Only do this if what edit or chat for this user is very recent.
521
# Otherwise we touch the project just for seeing notifications or opening
522
# the file, which is confusing and wastes a lot of resources.
523
x = obj.users?[account_id]
524
recent = misc.minutes_ago(3)
525
if x? and (x.edit >= recent or x.chat >= recent)
526
db.touch(project_id:obj.project_id, account_id:account_id)
527
cb?()
528
529
schema.hub_servers =
530
primary_key : 'host'
531
durability : 'soft' # loss of some log data not serious, since ephemeral and expires quickly anyways
532
fields:
533
host :
534
type : 'string'
535
pg_type : 'VARCHAR(63)'
536
port :
537
type : 'integer'
538
clients :
539
type : 'integer'
540
expire :
541
type : 'timestamp'
542
543
schema.instances =
544
primary_key: 'name'
545
fields:
546
name :
547
type : 'string'
548
gce :
549
type : 'map'
550
gce_sha1 :
551
type : 'string'
552
requested_preemptible :
553
type : 'boolean'
554
requested_status :
555
type : 'string'
556
desc : "One of 'RUNNING', 'TERMINATED'"
557
action :
558
type : 'map'
559
desc : "{action:'start', started:timestamp, finished:timestamp, params:?, error:?, rule:?}"
560
date : ['started', 'finished']
561
562
schema.instance_actions_log =
563
primary_key: 'id'
564
fields:
565
id :
566
type : 'uuid'
567
name :
568
type : 'string'
569
desc : 'hostname of vm'
570
pg_type : 'VARCHAR(63)'
571
action :
572
type : 'map'
573
desc : 'same as finished action object for instances above'
574
date : ['started', 'finished']
575
576
schema.passport_settings =
577
primary_key:'strategy'
578
fields:
579
strategy :
580
type : 'string'
581
conf :
582
type : 'map'
583
584
schema.password_reset =
585
primary_key: 'id'
586
fields:
587
id :
588
type : 'uuid'
589
email_address :
590
type : 'string'
591
expire :
592
type : 'timestamp'
593
594
schema.password_reset_attempts =
595
primary_key: 'id'
596
durability : 'soft' # loss not serious, since used only for analytics and preventing attacks
597
fields:
598
id :
599
type : 'uuid'
600
email_address :
601
type : 'string'
602
ip_address :
603
type : 'string'
604
pg_type : 'inet'
605
time :
606
type : 'timestamp'
607
pg_indexes: ['time']
608
609
schema.project_log =
610
primary_key: 'id'
611
durability : 'soft' # dropping a log entry (e.g., "foo opened a file") wouldn't matter much
612
fields :
613
id :
614
type : 'uuid'
615
desc : 'which'
616
project_id :
617
type : 'uuid'
618
desc : 'where'
619
time :
620
type : 'timestamp'
621
desc : 'when'
622
account_id :
623
type : 'uuid'
624
desc : 'who'
625
event :
626
type : 'map'
627
desc : 'what'
628
629
pg_indexes : ['project_id', 'time']
630
631
632
user_query:
633
get :
634
pg_where : 'projects'
635
pg_changefeed: 'projects'
636
options : [{order_by : '-time'}, {limit : 400}]
637
throttle_changes : 2000
638
fields :
639
id : null
640
project_id : null
641
time : null
642
account_id : null
643
event : null
644
set :
645
fields :
646
id : (obj) -> obj.id ? misc.uuid()
647
project_id : 'project_write'
648
account_id : 'account_id'
649
time : true
650
event : true
651
652
schema.projects =
653
primary_key: 'project_id'
654
fields :
655
project_id :
656
type : 'uuid',
657
desc : 'The project id, which is the primary key that determines the project.'
658
title :
659
type : 'string'
660
desc : 'The short title of the project. Should use no special formatting, except hashtags.'
661
description :
662
type : 'string'
663
desc : 'A longer textual description of the project. This can include hashtags and should be formatted using markdown.' # markdown rendering possibly not implemented
664
users :
665
type : 'map'
666
desc : "This is a map from account_id's to {hide:bool, group:['owner',...], upgrades:{memory:1000, ...}, ssh:{...}}."
667
invite :
668
type : 'map'
669
desc : "Map from email addresses to {time:when invite sent, error:error message if there was one}"
670
date : ['time']
671
invite_requests :
672
type : 'map'
673
desc : "This is a map from account_id's to {timestamp:?, message:'i want to join because...'}."
674
date : ['timestamp']
675
deleted :
676
type : 'boolean'
677
desc : 'Whether or not this project is deleted.'
678
host :
679
type : 'map'
680
desc : "This is a map {host:'hostname_of_server', assigned:timestamp of when assigned to that server}."
681
date : ['assigned']
682
settings :
683
type : 'map'
684
desc : 'This is a map that defines the free base quotas that a project has. It is of the form {cores: 1.5, cpu_shares: 768, disk_quota: 1000, memory: 2000, mintime: 36000000, network: 0}. WARNING: some of the values are strings not numbers in the database right now, e.g., disk_quota:"1000".'
685
status :
686
type : 'map'
687
desc : 'This is a map computed by the status command run inside a project, and slightly enhanced by the compute server, which gives extensive status information about a project. It has the form {console_server.pid: [pid of the console server, if running], console_server.port: [port if it is serving], disk_MB: [MB of used disk], installed: [whether code is installed], local_hub.pid: [pid of local hub server process], local_hub.port: [port of local hub process], memory: {count:?, pss:?, rss:?, swap:?, uss:?} [output by smem], raw.port: [port that the raw server is serving on], sage_server.pid: [pid of sage server process], sage_server.port: [port of the sage server], secret_token: [long random secret token that is needed to communicate with local_hub], state: "running" [see COMPUTE_STATES below], version: [version number of local_hub code]}'
688
state :
689
type : 'map'
690
desc : 'Info about the state of this project of the form {error: "", state: "running", time: timestamp}, where time is when the state was last computed. See COMPUTE_STATES below.'
691
date : ['time']
692
last_edited :
693
type : 'timestamp'
694
desc : 'The last time some file was edited in this project. This is the last time that the file_use table was updated for this project.'
695
last_active :
696
type : 'map'
697
desc : "Map from account_id's to the timestamp of when the user with that account_id touched this project."
698
date : 'all'
699
created :
700
type : 'timestamp'
701
desc : 'When the account was created.'
702
action_request :
703
type : 'map'
704
desc : "Request state change action for project: {action:['restart', 'stop', 'save', 'close'], started:timestamp, err:?, finished:timestamp}"
705
date : ['started', 'finished']
706
storage :
707
type : 'map'
708
desc : "This is a map {host:'hostname_of_server', assigned:when first saved here, saved:when last saved here}."
709
date : ['assigned', 'saved']
710
last_backup :
711
type : 'timestamp'
712
desc : "Timestamp of last off-disk successful backup using bup to Google cloud storage"
713
storage_request :
714
type : 'map'
715
desc : "{action:['save', 'close', 'move', 'open'], requested:timestap, pid:?, target:?, started:timestamp, finished:timestamp, err:?}"
716
date : ['started', 'finished', 'requested']
717
course :
718
type : 'map'
719
desc : '{project_id:[id of project that contains .course file], path:[path to .course file], pay:?, email_address:[optional email address of student -- used if account_id not known], account_id:[account id of student]}, where pay is either not set (or equals falseish) or is a timestamp by which the students must move the project to a members only server.'
720
date : ['pay']
721
storage_server :
722
type : 'integer'
723
desc : 'Number of the Kubernetes storage server with the data for this project: one of 0, 1, 2, ...'
724
storage_ready :
725
type : 'boolean'
726
desc : 'Whether storage is ready to be used on the storage server. Do NOT try to start project until true; this gets set by storage daemon when it notices the that run is true.'
727
disk_size :
728
type : 'integer'
729
desc : 'Size in megabytes of the project disk.'
730
resources :
731
type : 'map'
732
desc : 'Object of the form {requests:{memory:"30Mi",cpu:"5m"}, limits:{memory:"100Mi",cpu:"300m"}} which is passed to the k8s resources section for this pod.'
733
preemptible :
734
type : 'boolean'
735
desc : 'If true, allow to run on preemptible nodes.'
736
idle_timeout :
737
type : 'integer'
738
desc : 'If given and nonzero, project will be killed if it is idle for this many **minutes**, where idle *means* that last_edited has not been updated.'
739
run_quota :
740
type : 'map'
741
desc : 'If project is running, this is the quota that it is running with.'
742
743
pg_indexes : [
744
'last_edited',
745
'USING GIN (users)' # so get_collaborator_ids is fast
746
'USING GIN (host jsonb_path_ops)' # so get_projects_on_compute_server is fast
747
]
748
749
user_query:
750
get :
751
pg_where : 'projects'
752
pg_changefeed: 'projects'
753
throttle_changes : 2000
754
fields :
755
project_id : null
756
title : ''
757
description : ''
758
users : {}
759
invite : null # who has been invited to this project via email
760
invite_requests: null # who has requested to be invited
761
deleted : null
762
host : null
763
settings : DEFAULT_QUOTAS
764
status : null
765
state : null
766
last_edited : null
767
last_active : null
768
action_request : null # last requested action -- {action:?, time:?, started:?, finished:?, err:?}
769
course : null
770
set :
771
fields :
772
project_id : 'project_write'
773
title : true
774
description : true
775
deleted : true
776
invite_requests: true # project collabs can modify this (e.g., to remove from it once user added or rejected)
777
users : (obj, db, account_id) -> db._user_set_query_project_users(obj, account_id)
778
action_request : true # used to request that an action be performed, e.g., "save"; handled by before_change
779
780
before_change : (database, old_val, new_val, account_id, cb) ->
781
database._user_set_query_project_change_before(old_val, new_val, account_id, cb)
782
783
on_change : (database, old_val, new_val, account_id, cb) ->
784
database._user_set_query_project_change_after(old_val, new_val, account_id, cb)
785
786
project_query:
787
get :
788
pg_where : ["project_id = $::UUID" : 'project_id']
789
fields :
790
project_id : null
791
title : null
792
description : null
793
status : null
794
set :
795
fields :
796
project_id : 'project_id'
797
title : true
798
description : true
799
status : true
800
801
# Table that enables set queries to the course field of a project. Only
802
# project owners are allowed to use this table. The point is that this makes
803
# it possible for the owner of the project to set things, but not for the
804
# collaborators to set those things.
805
schema.projects_owner =
806
virtual : 'projects'
807
fields :
808
project_id : true
809
course : true
810
user_query :
811
set :
812
fields :
813
project_id : 'project_owner'
814
course : true
815
816
# Table that enables any signed-in user to set an invite request.
817
# Later: we can make an index so that users can see all outstanding requests they have made easily.
818
# How to test this from the browser console:
819
# project_id = '4e0f5bfd-3f1b-4d7b-9dff-456dcf8725b8' // id of a project you have
820
# invite_requests = {}; invite_requests[smc.client.account_id] = {timestamp:new Date(), message:'please invite me'}
821
# smc.client.query({cb:console.log, query:{project_invite_requests:{project_id:project_id, invite_requests:invite_requests}}}) // set it
822
# smc.redux.getStore('projects').get_project(project_id).invite_requests // see requests for this project
823
#
824
# CURRENTLY NOT USED.
825
schema.project_invite_requests =
826
virtual : 'projects'
827
primary_key: 'project_id'
828
fields :
829
project_id : true
830
invite_requests : true # {account_id:{timestamp:?, message:?}, ...}
831
user_query :
832
set :
833
fields :
834
project_id : true
835
invite_requests : true
836
before_change : (database, old_val, new_val, account_id, cb) ->
837
cb() # actual function will be database._user... as below.
838
#database._user_set_query_project_invite_requests(old_val, new_val, account_id, cb)
839
# For now don't check anything -- this is how we will make it secure later.
840
# This will:
841
# - that user setting this is signed in
842
# - ensure user only modifies their own entry (for their own id).
843
# - enforce some hard limit on number of outstanding invites (say 30).
844
# - enforce limit on size of invite message.
845
# - sanity check on timestamp
846
# - with an index as mentioned above we could limit the number of projects
847
# to which a single user has requested to be invited.
848
849
# Table that provides extended read info about a single project
850
# but *ONLY* for admin.
851
schema.projects_admin =
852
primary_key : schema.projects.primary_key
853
virtual : 'projects'
854
fields : schema.projects.fields
855
user_query:
856
get :
857
admin : true # only admins can do get queries on this table
858
# (without this, users who have read access could read)
859
pg_where : ['project_id = $::UUID':'project_id']
860
fields : schema.projects.user_query.get.fields
861
862
# Get publicly available information about a project.
863
#
864
schema.public_projects =
865
anonymous : true
866
virtual : 'projects'
867
user_query :
868
get :
869
pg_where : ['project_id = $::UUID':'project_id-public']
870
fields :
871
project_id : true
872
title : true
873
description : true
874
875
schema.public_paths =
876
primary_key : 'id'
877
anonymous : true # allow user *read* access, even if not signed in
878
fields:
879
id :
880
type : 'string'
881
pg_type : 'CHAR(40)'
882
desc : 'sha1 hash derived from project_id and path'
883
project_id :
884
type : 'uuid'
885
path :
886
type : 'string'
887
description :
888
type : 'string'
889
disabled :
890
type : 'boolean'
891
desc : 'if true then disabled'
892
pg_indexes : ['project_id']
893
user_query:
894
get :
895
pg_where : ["project_id = $::UUID": 'project_id']
896
throttle_changes : 2000
897
fields :
898
id : null
899
project_id : null
900
path : null
901
description : null
902
disabled : null # if true then disabled
903
set :
904
fields :
905
id : (obj, db) -> db.sha1(obj.project_id, obj.path)
906
project_id : 'project_write'
907
path : true
908
description : true
909
disabled : true
910
required_fields :
911
id : true
912
project_id : true
913
path : true
914
915
###
916
Requests and status related to copying files between projects.
917
###
918
schema.copy_paths =
919
primary_key : 'id'
920
fields:
921
id :
922
type : 'uuid'
923
desc : 'random unique id assigned to this copy request'
924
time :
925
type : 'timestamp'
926
desc : 'when this request was made'
927
source_project_id :
928
type : 'uuid'
929
desc : 'the project_id of the source project'
930
source_path :
931
type : 'string'
932
desc : 'the path of the source file or directory'
933
target_project_id :
934
type : 'uuid'
935
desc : 'the project_id of the target project'
936
target_path :
937
type : 'string'
938
desc : 'the path of the target file or directory'
939
overwrite_newer :
940
type : 'boolean'
941
desc : 'if new, overwrite newer files in destination'
942
delete_missing :
943
type : 'boolean'
944
desc : "if true, delete files in the target that aren't in the source path"
945
backup :
946
type : 'boolean'
947
desc : 'if true, make backup of files before overwriting'
948
bwlimit :
949
type : 'string'
950
desc : 'optional limit on the bandwidth dedicated to this copy (passed to rsync)'
951
timeout :
952
type : 'number'
953
desc : 'fail if the transfer itself takes longer than this number of seconds (passed to rsync)'
954
started :
955
type : 'timestamp'
956
desc : 'when the copy request actually started running'
957
finished :
958
type : 'timestamp'
959
desc : 'when the copy request finished'
960
error :
961
type : 'string'
962
desc : 'if the copy failed or output any errors, they are put here.'
963
pg_indexes : ['time']
964
# TODO: for now there are no user queries -- this is used entirely by backend servers,
965
# actually only in kucalc; later that may change, so the user can make copy
966
# requests this way, check on their status, show all current copies they are
967
# causing in a page (that is persistent over browser refreshes, etc.).
968
# That's for later.
969
970
971
schema.remember_me =
972
primary_key : 'hash'
973
durability : 'soft' # dropping this would just require a user to login again
974
fields :
975
hash :
976
type : 'string'
977
pg_type : 'CHAR(127)'
978
value :
979
type : 'map'
980
account_id :
981
type : 'uuid'
982
expire :
983
type : 'timestamp'
984
pg_indexes : ['account_id']
985
986
schema.auth_tokens =
987
primary_key : 'auth_token'
988
fields :
989
auth_token :
990
type : 'string'
991
pg_type : 'CHAR(24)'
992
account_id :
993
type : 'uuid'
994
expire :
995
type : 'timestamp'
996
997
schema.server_settings =
998
primary_key : 'name'
999
anonymous : false
1000
fields :
1001
name :
1002
type : 'string'
1003
value :
1004
type : 'string'
1005
user_query:
1006
# NOTE: can *set* but cannot get!
1007
set:
1008
admin : true
1009
fields:
1010
name : null
1011
value : null
1012
1013
# Default settings to customize a given site, typically a private install of SMC.
1014
exports.site_settings_conf =
1015
site_name:
1016
name : "Site name"
1017
desc : "The heading name of your site."
1018
default : "CoCalc"
1019
site_description:
1020
name : "Site description"
1021
desc : "The description of your site."
1022
default : ""
1023
terms_of_service:
1024
name : "Terms of service link text"
1025
desc : "The text displayed for the terms of service link (make empty to not require)."
1026
default : 'By signing up you agree to our <a target="_blank" href="/policies/terms.html">Terms of Service</a>.'
1027
account_creation_email_instructions:
1028
name : 'Account creation instructions'
1029
desc : "Instructions displayed next to the box where a user creates their account using their name and email address."
1030
default : 'Create an Account'
1031
help_email:
1032
name : "Help email address"
1033
desc : "Email address that user is directed to use for support requests"
1034
default : "[email protected]"
1035
commercial:
1036
name : "Commercial UI elements ('yes' or 'no')"
1037
desc : "Whether or not to include user interface elements related to for-pay upgrades and features. Set to 'yes' to include these elements."
1038
default : "no"
1039
kucalc:
1040
name : "KuCalc UI elements ('yes' or 'no')"
1041
desc : "Whether to show UI elements adapted to what the KuCalc backend provides"
1042
default : "no" # TODO -- this will *default* to yes when run from kucalc; but site admin can set it either way anywhere for testing.
1043
1044
1045
site_settings_fields = misc.keys(exports.site_settings_conf)
1046
1047
schema.site_settings =
1048
virtual : 'server_settings'
1049
anonymous : false
1050
user_query:
1051
# NOTE: can set and get only fields in site_settings_fields, but not any others.
1052
get:
1053
pg_where: ['name = ANY($)': site_settings_fields]
1054
admin : true
1055
fields :
1056
name : null
1057
value : null
1058
set:
1059
admin : true
1060
fields:
1061
name : (obj, db) ->
1062
if obj.name in site_settings_fields
1063
return obj.name
1064
throw Error("setting name='#{obj.name}' not allowed")
1065
value : null
1066
1067
schema.stats =
1068
primary_key : 'id'
1069
durability : 'soft' # ephemeral stats whose slight loss wouldn't matter much
1070
anonymous : true # allow user read access, even if not signed in
1071
fields:
1072
id :
1073
type : 'uuid'
1074
time :
1075
type : 'timestamp'
1076
pg_check : 'NOT NULL'
1077
accounts :
1078
type : 'integer'
1079
pg_check : 'NOT NULL CHECK (accounts >= 0)'
1080
accounts_created :
1081
type : 'map'
1082
files_opened :
1083
type : 'map'
1084
projects :
1085
type : 'integer'
1086
pg_check : 'NOT NULL CHECK (projects >= 0)'
1087
projects_created :
1088
type : 'map'
1089
projects_edited :
1090
type : 'map'
1091
hub_servers :
1092
type : 'array'
1093
pg_type : 'JSONB[]'
1094
pg_indexes : ['time']
1095
user_query:
1096
get:
1097
pg_where: ["time >= NOW() - INTERVAL '1 hour'"]
1098
pg_changefeed : 'one-hour'
1099
options : [{'order_by':'-time'}]
1100
throttle_changes : 5000
1101
fields :
1102
id : null
1103
time : null
1104
accounts : 0
1105
accounts_created : null
1106
projects : 0
1107
projects_created : null
1108
projects_edited : null
1109
hub_servers : []
1110
1111
schema.storage_servers =
1112
primary_key : 'host'
1113
fields :
1114
host :
1115
type : 'string'
1116
desc : 'hostname of the storage server'
1117
pg_type : 'VARCHAR(63)'
1118
1119
schema.system_notifications =
1120
primary_key : 'id'
1121
anonymous : true # allow users read access, even if not signed in
1122
fields :
1123
id :
1124
type : 'uuid'
1125
desc : 'primary key'
1126
time :
1127
type : 'timestamp'
1128
desc : 'time of this message'
1129
text :
1130
type : 'string'
1131
desc : 'the text of the message'
1132
priority:
1133
type : 'string'
1134
pg_type : 'VARCHAR(6)'
1135
desc : 'one of "low", "medium", or "high"'
1136
done:
1137
type : 'boolean'
1138
desc : 'if true, then this notification is no longer relevant'
1139
user_query:
1140
get:
1141
pg_where: ["time >= NOW() - INTERVAL '1 hour'"]
1142
pg_changefeed : 'one-hour'
1143
throttle_changes : 3000
1144
fields :
1145
id : null
1146
time : null
1147
text : ''
1148
priority : 'low'
1149
done : false
1150
set:
1151
admin : true
1152
fields:
1153
id : true
1154
time : true
1155
text : true
1156
priority : true
1157
done : true
1158
1159
1160
# Client side versions of some db functions, which are used, e.g., when setting fields.
1161
sha1 = require('sha1')
1162
class ClientDB
1163
constructor: ->
1164
@r = {}
1165
1166
sha1: (args...) =>
1167
v = ((if typeof(x) == 'string' then x else JSON.stringify(x)) for x in args).join('')
1168
return sha1(v)
1169
1170
_user_set_query_project_users: (obj) =>
1171
# client allows anything; server may be more stringent
1172
return obj.users
1173
1174
_user_set_query_project_change_after: (obj, old_val, new_val, cb) =>
1175
cb()
1176
_user_set_query_project_change_before: (obj, old_val, new_val, cb) =>
1177
cb()
1178
1179
primary_keys: (table) =>
1180
@_primary_keys_cache ?= {}
1181
if @_primary_keys_cache[table]?
1182
return @_primary_keys_cache[table]
1183
t = schema[table]
1184
if t.virtual?
1185
t = schema[t.virtual]
1186
v = t?.primary_key
1187
if not v?
1188
throw Error("primary key for table '#{table}' must be explicitly specified in schema")
1189
if typeof(v) == 'string'
1190
return @_primary_keys_cache[table] = [v]
1191
else if misc.is_array(v)
1192
if v.length == 0
1193
throw Error("at least one primary key must specified")
1194
return @_primary_keys_cache[table] = v
1195
else
1196
throw Error("primary key must be a string or array of strings")
1197
1198
1199
exports.client_db = new ClientDB()
1200
1201
1202