]>
Commit | Line | Data |
---|---|---|
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 | */ | |
40 | Ext.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 | }); |