]> git.proxmox.com Git - sencha-touch.git/blame - src/src/data/proxy/Sql.js
import Sencha Touch 2.4.2 source
[sencha-touch.git] / src / src / data / proxy / Sql.js
CommitLineData
c4685c84
TL
1/**
2 * SQL proxy lets you store data in a SQL database.
3 * The Sencha Touch SQL proxy outputs model data into an HTML5
4 * local database using WebSQL.
5 *
6 * You can create a Store for the proxy, for example:
7 *
8 * Ext.require(["Ext.data.proxy.SQL"]);
9 *
10 * Ext.define("User", {
11 * extend: "Ext.data.Model",
12 * config: {
13 * fields: [ "firstName", "lastName" ]
14 * }
15 * });
16 *
17 * Ext.create("Ext.data.Store", {
18 * model: "User",
19 * storeId: "Users",
20 * proxy: {
21 * type: "sql"
22 * }
23 * });
24 *
25 * Ext.getStore("Users").add({
26 * firstName: "Polly",
27 * lastName: "Hedra"
28 * });
29 *
30 * Ext.getStore("Users").sync();
31 *
32 * To destroy a table use:
33 *
34 * Ext.getStore("Users").getProxy().dropTable();
35 *
36 * To recreate a table use:
37 *
38 * Ext.data.Store.sync() or Ext.data.Model.save()
39 */
40Ext.define('Ext.data.proxy.Sql', {
41 alias: 'proxy.sql',
42 extend: 'Ext.data.proxy.Client',
43 alternateClassName: 'Ext.data.proxy.SQL',
44
45 isSQLProxy: true,
46
47 config: {
48 /**
49 * @cfg {Object} reader
50 * @hide
51 */
52 reader: null,
53 /**
54 * @cfg {Object} writer
55 * @hide
56 */
57 writer: null,
58 /**
59 * @cfg {String} table
60 * Optional Table name to use if not provided ModelName will be used
61 */
62 table: null,
63 /**
64 * @cfg {String} database
65 * Database name to access tables from
66 */
67 database: 'Sencha',
68
69 columns: '',
70
71 uniqueIdStrategy: false,
72
73 tableExists: false,
74
75 defaultDateFormat: 'Y-m-d H:i:s.u'
76 },
77
78 updateModel: function(model) {
79 if (model) {
80 var modelName = model.modelName,
81 defaultDateFormat = this.getDefaultDateFormat(),
82 table = modelName.slice(modelName.lastIndexOf('.') + 1);
83
84 model.getFields().each(function (field) {
85 if (field.getType().type === 'date' && !field.getDateFormat()) {
86 field.setDateFormat(defaultDateFormat);
87 }
88 });
89
90 this.setUniqueIdStrategy(model.getIdentifier().isUnique);
91 if (!this.getTable()) {
92 this.setTable(table);
93 }
94 this.setColumns(this.getPersistedModelColumns(model));
95 }
96
97 this.callParent(arguments);
98 },
99
100 setException: function(operation, error) {
101 operation.setException(error);
102 },
103
104 create: function (operation, callback, scope) {
105 var me = this,
106 db = me.getDatabaseObject(),
107 records = operation.getRecords(),
108 tableExists = me.getTableExists();
109
110 operation.setStarted();
111
112 db.transaction(function(transaction) {
113 if (!tableExists) {
114 me.createTable(transaction);
115 }
116
117 me.insertRecords(records, transaction, function(resultSet, error) {
118 if (operation.process(operation.getAction(), resultSet) === false) {
119 me.fireEvent('exception', me, operation);
120 }
121
122 if (error) {
123 operation.setException(error);
124 }
125 }, me);
126 },
127 function(transaction, error) {
128 me.setException(operation, error);
129 if (typeof callback == 'function') {
130 callback.call(scope || me, operation);
131 }
132 },
133 function(transaction) {
134 if (typeof callback == 'function') {
135 callback.call(scope || me, operation);
136 }
137 }
138 );
139 },
140
141 read: function(operation, callback, scope) {
142 var me = this,
143 db = me.getDatabaseObject(),
144 model = me.getModel(),
145 idProperty = model.getIdProperty(),
146 tableExists = me.getTableExists(),
147 params = operation.getParams() || {},
148 id = params[idProperty],
149 sorters = operation.getSorters(),
150 filters = operation.getFilters(),
151 page = operation.getPage(),
152 start = operation.getStart(),
153 limit = operation.getLimit(),
154 filtered, i, ln;
155
156 params = Ext.apply(params, {
157 page: page,
158 start: start,
159 limit: limit,
160 sorters: sorters,
161 filters: filters
162 });
163
164 operation.setStarted();
165
166 db.transaction(function(transaction) {
167 if (!tableExists) {
168 me.createTable(transaction);
169 }
170
171 me.selectRecords(transaction, id !== undefined ? id : params, function (resultSet, error) {
172 if (operation.process(operation.getAction(), resultSet) === false) {
173 me.fireEvent('exception', me, operation);
174 }
175
176 if (error) {
177 operation.setException(error);
178 }
179
180 if (filters && filters.length) {
181 filtered = Ext.create('Ext.util.Collection', function(record) {
182 return record.getId();
183 });
184 filtered.setFilterRoot('data');
185 for (i = 0, ln = filters.length; i < ln; i++) {
186 if (filters[i].getProperty() === null) {
187 filtered.addFilter(filters[i]);
188 }
189 }
190 filtered.addAll(operation.getRecords());
191
192 operation.setRecords(filtered.items.slice());
193 resultSet.setRecords(operation.getRecords());
194 resultSet.setCount(filtered.items.length);
195 resultSet.setTotal(filtered.items.length);
196 }
197 });
198 },
199 function(transaction, error) {
200 me.setException(operation, error);
201 if (typeof callback == 'function') {
202 callback.call(scope || me, operation);
203 }
204 },
205 function(transaction) {
206 if (typeof callback == 'function') {
207 callback.call(scope || me, operation);
208 }
209 }
210 );
211 },
212
213 update: function(operation, callback, scope) {
214 var me = this,
215 records = operation.getRecords(),
216 db = me.getDatabaseObject(),
217 tableExists = me.getTableExists();
218
219 operation.setStarted();
220
221 db.transaction(function (transaction) {
222 if (!tableExists) {
223 me.createTable(transaction);
224 }
225
226 me.updateRecords(transaction, records, function(resultSet, errors) {
227 if (operation.process(operation.getAction(), resultSet) === false) {
228 me.fireEvent('exception', me, operation);
229 }
230
231 if (errors) {
232 operation.setException(errors);
233 }
234 });
235 },
236 function(transaction, error) {
237 me.setException(operation, error);
238 if (typeof callback == 'function') {
239 callback.call(scope || me, operation);
240 }
241 },
242 function(transaction) {
243 if (typeof callback == 'function') {
244 callback.call(scope || me, operation);
245 }
246 }
247 );
248 },
249
250 destroy: function(operation, callback, scope) {
251 var me = this,
252 records = operation.getRecords(),
253 db = me.getDatabaseObject(),
254 tableExists = me.getTableExists();
255
256 operation.setStarted();
257
258 db.transaction(function(transaction) {
259 if (!tableExists) {
260 me.createTable(transaction);
261 }
262
263 me.destroyRecords(transaction, records, function(resultSet, error) {
264 if (operation.process(operation.getAction(), resultSet) === false) {
265 me.fireEvent('exception', me, operation);
266 }
267
268 if (error) {
269 operation.setException(error);
270 }
271 });
272 },
273 function(transaction, error) {
274 me.setException(operation, error);
275 if (typeof callback == 'function') {
276 callback.call(scope || me, operation);
277 }
278 },
279 function(transaction) {
280 if (typeof callback == 'function') {
281 callback.call(scope || me, operation);
282 }
283 }
284 );
285 },
286
287 createTable: function (transaction) {
288 transaction.executeSql('CREATE TABLE IF NOT EXISTS ' + this.getTable() + ' (' + this.getSchemaString() + ')');
289 this.setTableExists(true);
290 },
291
292 insertRecords: function(records, transaction, callback, scope) {
293 var me = this,
294 table = me.getTable(),
295 columns = me.getColumns(),
296 totalRecords = records.length,
297 executed = 0,
298 tmp = [],
299 insertedRecords = [],
300 errors = [],
301 uniqueIdStrategy = me.getUniqueIdStrategy(),
302 i, ln, placeholders, result;
303
304 result = new Ext.data.ResultSet({
305 records: insertedRecords,
306 success: true
307 });
308
309 for (i = 0, ln = columns.length; i < ln; i++) {
310 tmp.push('?');
311 }
312 placeholders = tmp.join(', ');
313
314 Ext.each(records, function (record) {
315 var id = record.getId(),
316 data = me.getRecordData(record),
317 values = me.getColumnValues(columns, data);
318
319 transaction.executeSql(
320 'INSERT INTO ' + table + ' (' + columns.join(', ') + ') VALUES (' + placeholders + ')', values,
321 function (transaction, resultSet) {
322 executed++;
323 insertedRecords.push({
324 clientId: id,
325 id: uniqueIdStrategy ? id : resultSet.insertId,
326 data: data,
327 node: data
328 });
329
330 if (executed === totalRecords && typeof callback == 'function') {
331 callback.call(scope || me, result, errors.length > 0 ? errors : null);
332 }
333 },
334 function (transaction, error) {
335 executed++;
336 errors.push({
337 clientId: id,
338 error: error
339 });
340
341 if (executed === totalRecords && typeof callback == 'function') {
342 callback.call(scope || me, result, errors);
343 }
344 }
345 );
346 });
347 },
348
349 selectRecords: function(transaction, params, callback, scope) {
350 var me = this,
351 table = me.getTable(),
352 idProperty = me.getModel().getIdProperty(),
353 sql = 'SELECT * FROM ' + table,
354 records = [],
355 filterStatement = ' WHERE ',
356 sortStatement = ' ORDER BY ',
357 i, ln, data, result, count, rows, filter, sorter, property, value;
358
359 result = new Ext.data.ResultSet({
360 records: records,
361 success: true
362 });
363
364 if (!Ext.isObject(params)) {
365 sql += filterStatement + idProperty + ' = ' + params;
366 } else {
367 ln = params.filters && params.filters.length;
368 if (ln) {
369 for (i = 0; i < ln; i++) {
370 filter = params.filters[i];
371 property = filter.getProperty();
372 value = filter.getValue();
373 if (property !== null) {
374 sql += filterStatement + property + ' ' + (filter.getAnyMatch() ? ('LIKE \'%' + value + '%\'') : ('= \'' + value + '\''));
375 filterStatement = ' AND ';
376 }
377 }
378 }
379
380 ln = params.sorters && params.sorters.length;
381 if (ln) {
382 for (i = 0; i < ln; i++) {
383 sorter = params.sorters[i];
384 property = sorter.getProperty();
385 if (property !== null) {
386 sql += sortStatement + property + ' ' + sorter.getDirection();
387 sortStatement = ', ';
388 }
389 }
390 }
391
392 // handle start, limit, sort, filter and group params
393 if (params.page !== undefined) {
394 sql += ' LIMIT ' + parseInt(params.start, 10) + ', ' + parseInt(params.limit, 10);
395 }
396 }
397 transaction.executeSql(sql, null,
398 function(transaction, resultSet) {
399 rows = resultSet.rows;
400 count = rows.length;
401
402 for (i = 0, ln = count; i < ln; i++) {
403 data = rows.item(i);
404 records.push({
405 clientId: null,
406 id: data[idProperty],
407 data: data,
408 node: data
409 });
410 }
411
412 result.setSuccess(true);
413 result.setTotal(count);
414 result.setCount(count);
415
416 if (typeof callback == 'function') {
417 callback.call(scope || me, result);
418 }
419 },
420 function(transaction, error) {
421 result.setSuccess(false);
422 result.setTotal(0);
423 result.setCount(0);
424
425 if (typeof callback == 'function') {
426 callback.call(scope || me, result, error);
427 }
428 }
429 );
430 },
431
432 updateRecords: function (transaction, records, callback, scope) {
433 var me = this,
434 table = me.getTable(),
435 columns = me.getColumns(),
436 totalRecords = records.length,
437 idProperty = me.getModel().getIdProperty(),
438 executed = 0,
439 updatedRecords = [],
440 errors = [],
441 i, ln, result;
442
443 result = new Ext.data.ResultSet({
444 records: updatedRecords,
445 success: true
446 });
447
448 Ext.each(records, function (record) {
449 var id = record.getId(),
450 data = me.getRecordData(record),
451 values = me.getColumnValues(columns, data),
452 updates = [];
453
454 for (i = 0, ln = columns.length; i < ln; i++) {
455 updates.push(columns[i] + ' = ?');
456 }
457
458 transaction.executeSql(
459 'UPDATE ' + table + ' SET ' + updates.join(', ') + ' WHERE ' + idProperty + ' = ?', values.concat(id),
460 function (transaction, resultSet) {
461 executed++;
462 updatedRecords.push({
463 clientId: id,
464 id: id,
465 data: data,
466 node: data
467 });
468
469 if (executed === totalRecords && typeof callback == 'function') {
470 callback.call(scope || me, result, errors.length > 0 ? errors : null);
471 }
472 },
473 function (transaction, error) {
474 executed++;
475 errors.push({
476 clientId: id,
477 error: error
478 });
479
480 if (executed === totalRecords && typeof callback == 'function') {
481 callback.call(scope || me, result, errors);
482 }
483 }
484 );
485 });
486 },
487
488 destroyRecords: function (transaction, records, callback, scope) {
489 var me = this,
490 table = me.getTable(),
491 idProperty = me.getModel().getIdProperty(),
492 ids = [],
493 values = [],
494 destroyedRecords = [],
495 i, ln, result, record;
496
497 for (i = 0, ln = records.length; i < ln; i++) {
498 ids.push(idProperty + ' = ?');
499 values.push(records[i].getId());
500 }
501
502 result = new Ext.data.ResultSet({
503 records: destroyedRecords,
504 success: true
505 });
506
507 transaction.executeSql(
508 'DELETE FROM ' + table + ' WHERE ' + ids.join(' OR '), values,
509 function (transaction, resultSet) {
510 for (i = 0, ln = records.length; i < ln; i++) {
511 record = records[i];
512 destroyedRecords.push({
513 id: record.getId()
514 });
515 }
516
517 if (typeof callback == 'function') {
518 callback.call(scope || me, result);
519 }
520 },
521 function (transaction, error) {
522 if (typeof callback == 'function') {
523 callback.call(scope || me, result, error);
524 }
525 }
526 );
527 },
528
529 /**
530 * Formats the data for each record before sending it to the server. This
531 * method should be overridden to format the data in a way that differs from the default.
532 * @param {Object} record The record that we are writing to the server.
533 * @return {Object} An object literal of name/value keys to be written to the server.
534 * By default this method returns the data property on the record.
535 */
536 getRecordData: function (record) {
537 var me = this,
538 fields = record.getFields(),
539 idProperty = record.getIdProperty(),
540 uniqueIdStrategy = me.getUniqueIdStrategy(),
541 data = {},
542 name, value;
543
544 fields.each(function (field) {
545 if (field.getPersist()) {
546 name = field.getName();
547 if (name === idProperty && !uniqueIdStrategy) {
548 return;
549 }
550 value = record.get(name);
551 if (field.getType().type == 'date') {
552 value = me.writeDate(field, value);
553 }
554 data[name] = value;
555 }
556 }, me);
557
558 return data;
559 },
560
561 getColumnValues: function(columns, data) {
562 var ln = columns.length,
563 values = [],
564 i, column, value;
565
566 for (i = 0; i < ln; i++) {
567 column = columns[i];
568 value = data[column];
569 if (value !== undefined) {
570 values.push(value);
571 }
572 }
573
574 return values;
575 },
576
577 getSchemaString: function() {
578 var me = this,
579 schema = [],
580 model = me.getModel(),
581 idProperty = model.getIdProperty(),
582 fields = model.getFields().items,
583 uniqueIdStrategy = me.getUniqueIdStrategy(),
584 ln = fields.length,
585 i, field, type, name;
586
587 for (i = 0; i < ln; i++) {
588 field = fields[i];
589 type = field.getType().type;
590 name = field.getName();
591
592 if (name === idProperty) {
593 if (uniqueIdStrategy) {
594 type = me.convertToSqlType(type);
595 schema.unshift(idProperty + ' ' + type);
596 } else {
597 schema.unshift(idProperty + ' INTEGER PRIMARY KEY AUTOINCREMENT');
598 }
599 } else {
600 type = me.convertToSqlType(type);
601 schema.push(name + ' ' + type);
602 }
603 }
604
605 return schema.join(', ');
606 },
607
608 getPersistedModelColumns: function(model) {
609 var fields = model.getFields().items,
610 uniqueIdStrategy = this.getUniqueIdStrategy(),
611 idProperty = model.getIdProperty(),
612 columns = [],
613 ln = fields.length,
614 i, field, name;
615
616 for (i = 0; i < ln; i++) {
617 field = fields[i];
618 name = field.getName();
619
620 if (name === idProperty && !uniqueIdStrategy) {
621 continue;
622 }
623
624 if (field.getPersist()) {
625 columns.push(field.getName());
626 }
627 }
628 return columns;
629 },
630
631 convertToSqlType: function(type) {
632 switch (type.toLowerCase()) {
633 case 'date':
634 case 'string':
635 case 'auto':
636 return 'TEXT';
637 case 'int':
638 return 'INTEGER';
639 case 'float':
640 return 'REAL';
641 case 'bool':
642 return 'NUMERIC';
643 }
644 },
645
646 writeDate: function (field, date) {
647 if (Ext.isEmpty(date)) {
648 return null;
649 }
650
651 var dateFormat = field.getDateFormat() || this.getDefaultDateFormat();
652 switch (dateFormat) {
653 case 'timestamp':
654 return date.getTime() / 1000;
655 case 'time':
656 return date.getTime();
657 default:
658 return Ext.Date.format(date, dateFormat);
659 }
660 },
661
662 dropTable: function(config) {
663 var me = this,
664 table = me.getTable(),
665 callback = config ? config.callback : null,
666 scope = config ? config.scope || me : null,
667 db = me.getDatabaseObject();
668
669 db.transaction(function(transaction) {
670 transaction.executeSql('DROP TABLE ' + table);
671 },
672 function(transaction, error) {
673 if (typeof callback == 'function') {
674 callback.call(scope || me, false, table, error);
675 }
676 },
677 function(transaction) {
678 if (typeof callback == 'function') {
679 callback.call(scope || me, true, table);
680 }
681 }
682 );
683
684 me.setTableExists(false);
685 },
686
687 getDatabaseObject: function() {
688 return openDatabase(this.getDatabase(), '1.0', 'Sencha Database', 5 * 1024 * 1024);
689 }
690});